おぴよの気まぐれ日記

おぴよの気まぐれ日記

岡山やプログラミング、ファッションのこと、子育てや人生、生き方についての備忘録。

【完全版】RubyonRailsのActiveRecord基礎!

こんにちは。opiyoです。

今日はRailsの勉強をしていると出てくる「Active Record 」について、勉強したいと思います。

  • User.first
  • User.find(1)
  • User.update
  • User.create!(hoge: hoge)

こんな奴らですね。

ではでは早速、行ってみましょう。

railsでデータを取得するActiveRecordとは

Ruby on Railsで使われているO/Rマッパー。データベースからデータを取り出すときのアプローチの一つ。

O/Rマッパーとは

  • 「オブジェクトリレーショナルマッピング」の略。 (ORMと略されることもある)
  • オブジェクトをリレーショナルデータベース(RDBMS)のテーブルに接続するもの
  • ORMを使用することで、SQL文を直接書かなくて良い
  • わずかなコードで、オブジェクトの属性やリレーションシップをデータベースに保存/読み出しができる

ActiveRecord命名ルール

  • モデル/クラス名:単数形、テーブル名:複数形
  • モデル/クラス:2語以上の場合はキャメルケース(語頭を大文字にしてスペースなしでつなぐ)
  • テーブル:小文字かつアンダースコアで区切る
モデル/クラス名 テーブル/スキーマ
Post posts
LineTime line_time
Person people

スキーマのルール

  • 外部キー:テーブル名の単数形_idにする
  • 主キー:デフォルトはidのカラム

モデルの作成

Userモデルを作成するにはApplicationRecordクラスのサブクラスを作成します。

SQLでテーブルを作成するとこうなります。

CREATE TABLE products (
   id int(11) NOT NULL auto_increment,
   name varchar(255),
   PRIMARY KEY  (id)
);

後から出てくるマイグレーションを使うとコマンドを使ってモデルの作成が可能です。

$ rails g model Procust name:string

CRUD データの読み書き

登録(Create)

  • newメソッドは新しい「オブジェクト」を作成する
  • createメソッドはデータベースに保存される
user = Usre.create(name: "David", email: "kosmo.waizu0804@gmail.com")

newメソッドを使う場合は、オブジェクトは保存されない。saveして保存する。

user = User.new
user.name = "David"
user.email = "kosmo.waizu0804@gmail.com"

user.save

一覧表示(Read)

# すべてのユーザーを返す
User.all
# 最初のユーザーを返す
User.first
# Davidという名前を持つ最初のユーザーを返す
david = User.find_by(name: 'David')
# 名前がDavidで、職業がコードアーティストのユーザーをすべて返し、created_atカラムで逆順ソートする
users = User.where(name: 'David', occupation: 'Code Artist').order('created_at DESC')

更新(update)

# saveメソッドを使う場合
user = User.find_by(name: "David")
user.name = 'Dave'
user.save
# updateメソッドを使う場合
user = User.find_by(name: "David")
user.update(name: 'Dave')
# 複数属性、複数レコード更新する場合
user.update_all "max_login_attempts = 3, must_change_password = 'true'"

削除(delete)

user = User.find_by(name: "David")
user.destroy

検証(validation)

  • ActiveRecordを使用すると、データベースに書き込まれる前に状態を検証することができる
  • 例えば
    • 空でないこと
    • 一意であること
    • すでにデータベースにないこと
  • save、updateメソッドは検証に失敗した場合は「false」を返す
class User < ApplicationRecord
  validates :name, presence: true # presenceは空を許さない
end
 
user = User.new 
user.save  # => false # 空のままsaveしてるので失敗 → false
user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank # 空のままsave!してるので失敗 → 例外

コールバック

データを作成、更新、登録、削除する前後に何かしら処理をしたい場合などに利用します。 例えばRailsチュートリアルでは、Userモデルを登録する前(before_save)でメールアドレスを小文字にするメソッドを呼び出す。などをしてます。

マイグレーション

Railsではデータベースの情報を履歴として管理する仕組みがあり、これをマイグレーション(migration)よ呼びます。 どのマイグレーションファイルが、データベースに反映されているかRailsは知っているので一つ前の状態に戻すなどが簡単にできます。

# db/migrate/20170629005430_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end
# db/migrate/20170806092710_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

実行方法はrails db:migrate。一つ前に戻る時はrails db:rollback。一からやり直したい時はrails db:migration:reset

これらをまとめた元ネタはRailsガイドになります。 今後も学んだことは追記し、充実させていければと思っております。

【完全版】RubyonRailsチュートリアルで人生を変える28歳の夏(演習問題の回答あり)

こんにちわ。opiyoです。

改めてちゃんと書こうと思いますが、一先ず今日。ってかさっき今月中を目標にしていたRailsチュートリアル14章までのまとめ記事をアップしました。

せっかくなので、まとめポストを書こうと思います。(今月の目標記事数に届かないから急いて書いてるってのは内緒な)

ではでは、どうぞー

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

opiyotan.hatenablog.com

Ruby on Rails チュートリアルで30歳までに人生を変える(第14章 完)

こんにちは。opiyoです。

今回は、第14章をやっていきます。

第14章はフォロー、フォロワーする機能を追加します。

なんとなんと最後の章までやってまいりました。

ではでは、早速行ってみましょう。

railsチュートリアル14章の演習解説

14.1.1 演習 データモデルの問題 (および解決策)

14.1.1.1

<問題> 図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。

<回答>

[2,7,10,8]

14.1.1.2

<問題>図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。

<回答>

[1]

14.1.2 演習 User/Relationshipの関連付け

14.1.2.1

<問題> コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください。

<回答>

> user_a.active_relationships.create(followed_id: user_b.id)
   (0.1ms)  begin transaction
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 101], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 102], ["LIMIT", 1]]
  SQL (7.1ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 101], ["followed_id", 102], ["created_at", 2017-08-31 01:06:19 UTC], ["updated_at", 2017-08-31 01:06:19 UTC]]
   (1.9ms)  commit transaction
=> #<Relationship:0x007fdb1fbac6b8 id: 88, follower_id: 101, followed_id: 102, created_at: Thu, 31 Aug 2017 01:06:19 UTC +00:00, updated_at: Thu, 31 Aug 2017 01:06:19 UTC +00:00>

14.1.2.2

<問題> 先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。

<回答> active_relationship.followedactive_relationship.followerを実行するとエラーになるので確認できない。

# 14.1.4 フォローしているユーザー で出てくるやり方を使えば、フォローしている、していないの確認ができそう!
> user_a.following?(user_b)
  User Exists (13.0ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 101], ["id", 102], ["LIMIT", 1]]
=> true

14.1.3 演習 Relationshipのバリデーション

14.1.3.1

<問題>リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。)

<回答> コメントアウトしてもテストは成功することを確認

14.1.4 演習 フォローしているユーザー

14.1.4.1

<問題>コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。

<回答>

> michael = User.find_by(name: "michael")
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "michael"], ["LIMIT", 1]]
=> #<User:0x007fdb1b727628
 id: 103,
 name: "michael",
 email: "michael@co.jp",
 created_at: Thu, 31 Aug 2017 02:00:21 UTC +00:00,
 updated_at: Thu, 31 Aug 2017 02:00:28 UTC +00:00,
 password_digest: "$2a$10$2hC.1.fUg3T8GUKMX.EnTOGDcIEHpHryFQg69YKmljRALWUeKsyYG",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$rglVP8eQVwnwE/diLFGPyuZJjA0lDGOtMwqSmewgeDIEdxU9N7Fk6",
 activated: true,
 activated_at: Thu, 31 Aug 2017 02:00:28 UTC +00:00,
 reset_digest: nil,
 reset_sent_at: nil>

[50] pry(main)> archer = User.find_by(name: "archer")
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "archer"], ["LIMIT", 1]]
=> #<User:0x007fdb19dfc1a8
 id: 104,
 name: "archer",
 email: "archer@co.jp",
 created_at: Thu, 31 Aug 2017 02:00:39 UTC +00:00,
 updated_at: Thu, 31 Aug 2017 02:00:41 UTC +00:00,
 password_digest: "$2a$10$QMPgyffkll/ps.Mh798Kd.97jtJa.gW3mjEGmLlAme5tmL//IhXJG",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$PygPxQjeBFgeXIKwPuQcqOIbwLevHEs2QXs5Tyvn6m05T7w7Vsc4C",
 activated: true,
 activated_at: Thu, 31 Aug 2017 02:00:41 UTC +00:00,
 reset_digest: nil,
 reset_sent_at: nil>


[51] pry(main)> michael.following?(archer)
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 103], ["id", 104], ["LIMIT", 1]]
=> false

[52] pry(main)> michael.follow(archer)
   (0.1ms)  begin transaction
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 103], ["LIMIT", 1]]
  User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 104], ["LIMIT", 1]]
  SQL (14.0ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 103], ["followed_id", 104], ["created_at", 2017-08-31 02:01:33 UTC], ["updated_at", 2017-08-31 02:01:33 UTC]]
   (1.9ms)  commit transaction
=> #<Relationship:0x007fdb221c9620 id: 89, follower_id: 103, followed_id: 104, created_at: Thu, 31 Aug 2017 02:01:33 UTC +00:00, updated_at: Thu, 31 Aug 2017 02:01:33 UTC +00:00>

[53] pry(main)> michael.following?(archer)
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 103], ["id", 104], ["LIMIT", 1]]
=> true

[54] pry(main)> michael.unfollow(archer)
  Relationship Load (0.2ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ?  [["follower_id", 103], ["followed_id", 104], ["LIMIT", 1]]
   (0.0ms)  begin transaction
  SQL (0.3ms)  DELETE FROM "relationships" WHERE "relationships"."id" = ?  [["id", 89]]
   (1.8ms)  commit transaction
=> #<Relationship:0x007fdb22bae5f0 id: 89, follower_id: 103, followed_id: 104, created_at: Thu, 31 Aug 2017 02:01:33 UTC +00:00, updated_at: Thu, 31 Aug 2017 02:01:33 UTC +00:00>

[55] pry(main)> michael.following?(archer)
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 103], ["id", 104], ["LIMIT", 1]]
=> false

14.1.4.2

<問題>先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。

<回答>

> michael.following?(archer)
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 103], ["id", 104], ["LIMIT", 1]]

> michael.follow(archer)
   (0.1ms)  begin transaction
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 103], ["LIMIT", 1]]
  User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 104], ["LIMIT", 1]]
  SQL (14.0ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 103], ["followed_id", 104], ["created_at", 2017-08-31 02:01:33 UTC], ["updated_at", 2017-08-31 02:01:33 UTC]]

> michael.unfollow(archer)
  Relationship Load (0.2ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ?  [["follower_id", 103], ["followed_id", 104], ["LIMIT", 1]]
  SQL (0.3ms)  DELETE FROM "relationships" WHERE "relationships"."id" = ?  [["id", 89]]

14.1.5 演習 フォロワー

14.1.5.1

<問題>コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?

<回答>

# ランダムに6人取ってくる
> users = User.order("RANDOM()").limit(6)

# その6人が一番目のユーザーをフォローする
> users.each {|user| user.follow(User.first)}

# 一番目のユーザーのフォロワーを確認
> User.first.followers.map(&:id)
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  User Load (0.2ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> [36, 20, 60, 74, 87, 53]

14.1.5.2

<問題>上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。

<回答>

> User.first.followers.count
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 6

14.1.5.2

<問題>user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。

<回答>

> User.first.followers.to_a.count
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  User Load (0.1ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 6

to_a付けても変化はない。が、多分配列にした数を数える感じになるから遅くなる。

14.2.1 演習 フォローのサンプルデータ

14.2.1.1

<問題>コンソールを開き、User.first.followers.countの結果がリスト 14.14で期待している結果と合致していることを確認してみましょう。

<回答>

> User.first.followers.count
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 38

> (3..40).size
=> 38

14.2.1.2

<問題>先ほどの演習と同様に、User.first.following.countの結果も合致していることを確認してみましょう。

<回答>

> User.first.following.count
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> 49

> (2..50).count
=> 49

14.2.2 演習 統計と [Follow] フォーム

14.2.2.1

<問題>ブラウザから /users/2 にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5 では [Unfollow] ボタンが表示されているはずです。さて、/users/1 にアクセスすると、どのような結果が表示されるでしょうか?

<回答> - /users/2:フォローボタンが表示 - /users/5:アンフォローボタンが表示 - /users/1:何も表示されない(ログインユーザーだから)

14.2.2.1

<問題>ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。

<回答> 表示されている。

14.2.2.1

<問題>Homeページに表示されている統計情報に対してテストを書いてみましょう。ヒント: リスト 13.28に示したテストを追加してみてください。同様にして、プロフィールページにもテストを追加してみましょう。

<回答>

# test/integration/users_profile_test.rb
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.following.count.to_s, response.body
    assert_match @user.followers.count.to_s, response.body
    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

14.2.3 演習 [Following] と [Followers] ページ

14.2.3.1

<問題>ブラウザから /users/1/followers と /users/1/following を開き、それぞれが適切に表示されていることを確認してみましょう。サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?

<回答> 表示されている。また、画像のリンクも機能している。

14.2.3.1

<問題>リスト 14.29のassert_selectに関連するコードをコメントアウトしてみて、テストが正しく red に変わることを確認してみましょう。

<回答>

# app/views/users/show_follow.html.erb
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>

多分ここだと思う。assert_selectで確認しているのはaタグのリンク先とユーザーのパス(users/5)が合致しているかだから。

14.2.4 演習 [Follow] ボタン (基本編)

14.2.4.1

<問題>ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?

<回答> followersの数字が変わるし、ボタンも変わるので機能している。

14.2.4.1

<問題>先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?

<回答> users/show.html.erbが描画される

14.2.5 演習 [Follow] ボタン (Ajax編)

14.2.5.1

<問題>ブラウザから /users/2 にアクセスし、うまく動いているかどうか確認してみましょう。

<回答> うまく動いていることを確認。

14.2.5.2

<問題>先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。

<回答> relationships/create.js.erbrelationships/destroy.js.erbが呼ばれているログが見つからない。

14.2.6 演習 フォローをテストする

14.2.6.1

<問題>リスト 14.36のrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認してみましょう。実際、どのテストケースが落ちたでしょうか?

<回答> - format.html { redirect_to @user and return}の場合は、htmlを呼び出すテストが失敗する - 28: "should follow a user the standard way" - 40: "should unfollow a user the standard way"

  • format.jsの場合は、jsを呼び出すテストが失敗しない

14.2.5.2といい、ちゃんと呼ばれてないのかな?

14.2.6.2

<問題>リスト 14.40のxhr: trueがある行のうち、片方のみを削除するとどういった結果になるでしょうか? このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのか考えてみてください。

<回答> format.htmlformat.jsのどちらかの行を削除した時ってことかな? その場合は、14.2.6.1の回答と同じになっちゃうな。 とりあえずパス。

14.3.1 演習 動機と計画

14.3.1.1

<問題>マイクロポストのidが正しく並んでいると仮定して (すなわち若いidの投稿ほど古くなる前提で)、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。ヒント: 13.1.4で実装したdefault_scopeを思い出してください。

<回答>

[10,9,7,5,4,2,1]

14.3.2 演習 フィードを初めて実装する

14.3.2.1

<問題>リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

<回答>

  def feed
    following_ids = "SELECT followed_id FROM relationships
      WHERE follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids})", user_id: id)
  end
# 取得件数が減ったからページネーションされないってことじゃないかな?
 FAIL["test_micropost_interface", MicropostsInterfaceTest, 1.4457496699978947]
 test_micropost_interface#MicropostsInterfaceTest (1.45s)
        Expected at least 1 element matching "div.pagination", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:12:in `block in <class:MicropostsInterfaceTest>'

# こっちが今回で欲しかったエラーだね
 FAIL["test_feed_should_have_the_right_posts", UserTest, 1.505586264996964]
 test_feed_should_have_the_right_posts#UserTest (1.51s)
        Expected false to be truthy.
        test/models/user_test.rb:106:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:105:in `block in <class:UserTest>'

14.3.2.2

<問題>リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

<回答>

def feed
  Micropost.where(user_id: id)
end
FAIL["test_feed_should_have_the_right_posts", UserTest, 3.7484980859990173]
 test_feed_should_have_the_right_posts#UserTest (3.75s)
        Expected false to be truthy.
        test/models/user_test.rb:102:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:101:in `block in <class:UserTest>'

14.3.2.3

<問題>リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか? ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。

<回答>

ようは全部!ってことだと思うんだが。

def feed
  Micropost.all
end
 FAIL["test_feed_should_have_the_right_posts", UserTest, 6.150512954001897]
 test_feed_should_have_the_right_posts#UserTest (6.15s)
        Expected true to be nil or false
        test/models/user_test.rb:110:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:109:in `block in <class:UserTest>'

14.3.3 演習 サブセレクト

14.3.3.1

<問題>Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。

<回答>

# test/integration/following_test.rb
  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match CGI.escapeHTML(micropost.content), response.body
    end
  end

14.3.3.2

<問題>リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています (このメソッドは11.2.3で扱ったCGI.escapeと同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能 (Cmd-FもしくはCtrl-F) を使って「sorry」を探すと原因の究明に役立つはずです。

<回答>

(byebug) CGI.escapeHTML(micropost.content)
"I&#39;m sorry. Your words made sense, but your sarcastic tone did not." # 「'」これがエスケープされる、されないの違い
(byebug) micropost.content
"I'm sorry. Your words made sense, but your sarcastic tone did not."

Ruby on Rails チュートリアルで30歳までに人生を変える(第13章)

こんにちは。opiyoです。

今回は、第13章をやっていきます。

第13章はユーザーが短いメッセージを投稿できる「マイクロポスト」機能を追加します。

やっとログイン関係の処理を抜けて、機能拡張ですね!

ではでは、早速行ってみましょう。

railsチュートリアル13章の演習解説

13.1.1 演習 基本的なモデル

13.1.1.1

<問題>RailsコンソールでMicropost.newを実行し、インスタンスを変数micropostに代入してください。その後、user_idに最初のユーザーのidを、contentに "Lorem ipsum" をそれぞれ代入してみてください。この時点では、 micropostオブジェクトのマジックカラム (created_atとupdated_at) には何が入っているでしょうか?

<回答>

> micropost = Micropost.new
=> #<Micropost:0x007fbbfc35b870 id: nil, content: nil, user_id: nil, created_at: nil, updated_at: nil, picture: nil>
[3] pry(main)> micropost.user_id = User.first.id
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> 1
[4] pry(main)> micropost.content = "Lorem ipsum"
=> "Lorem ipsum"
[5] pry(main)> micropost
=> #<Micropost:0x007fbbfc35b870 id: nil, content: "Lorem ipsum", user_id: 1, created_at: nil, updated_at: nil, picture: nil>

created_atとupdated_atには何も入ってない

13.1.1.2

<問題>先ほど作ったオブジェクトを使って、micropost.userを実行してみましょう。どのような結果が返ってくるでしょうか? また、micropost.user.nameを実行した場合の結果はどうなるでしょうか?

<回答>

> micropost.user
  User Load (0.6ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User:0x007fbc0396b358
 id: 1,
 name: "Example User",
 email: "example@railstutorial.org",
 created_at: Wed, 30 Aug 2017 01:05:28 UTC +00:00,
 updated_at: Wed, 30 Aug 2017 01:05:28 UTC +00:00,
 password_digest: "$2a$10$zjV8yVOjS/lZaq6t2I0Ae.xOrbyTAE/G18QeHKZw/72vRIFOOaSl6",
 remember_digest: nil,
 admin: true,
 activation_digest: "$2a$10$WVoTuhmujpxjL.agLrTGeeZy/56retMcUz2fYMe.8YWfBPf.8ZnRq",
 activated: true,
 activated_at: Wed, 30 Aug 2017 01:05:28 UTC +00:00,
 reset_digest: nil,
 reset_sent_at: nil>

[7] pry(main)> micropost.user.name
=> "Example User"

13.1.1.3

<問題>先ほど作ったmicropostオブジェクトをデータベースに保存してみましょう。この時点でもう一度マジックカラムの内容を調べてみましょう。今度はどのような値が入っているでしょうか?

<回答>

> micropost.save
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "Lorem ipsum"], ["user_id", 1], ["created_at", 2017-08-30 01:19:30 UTC], ["updated_at", 2017-08-30 01:19:30 UTC]]
   (2.5ms)  commit transaction
=> true
[11] pry(main)> micropost
=> #<Micropost:0x007fbbfc35b870 id: 301, content: "Lorem ipsum", user_id: 1, created_at: Wed, 30 Aug 2017 01:19:30 UTC +00:00, updated_at: Wed, 30 Aug 2017 01:19:30 UTC +00:00, picture: nil>

マジックカラムであるcreated_atとupdated_atに値が入った!!

13.1.2 演習 Micropostのバリデーション

13.1.2.1

<問題>Railsコンソールを開き、user_idとcontentが空になっているmicropostオブジェクトを作ってみてください。このオブジェクトに対してvalid?を実行すると、失敗することを確認してみましょう。また、生成されたエラーメッセージにはどんな内容が書かれているでしょうか?

<回答>

> micropost = Micropost.new
=> #<Micropost:0x007fbbfcdc41b8 id: nil, content: nil, user_id: nil, created_at: nil, updated_at: nil, picture: nil>
[2] pry(main)> micropost.valid?
=> false

> micropost.errors.full_messages
=> ["User must exist", "User can't be blank", "Content can't be blank"]

13.1.2.2

<問題>コンソールを開き、今度はuser_idが空でcontentが141文字以上のmicropostオブジェクトを作ってみてください。このオブジェクトに対してvalid?を実行すると、失敗することを確認してみましょう。また、生成されたエラーメッセージにはどんな内容が書かれているでしょうか?

<回答>

> micropost.content = "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
> micropost.content.size
=> 141

> micropost.valid?
=> false
[16] pry(main)> micropost.errors.full_messages
=> ["User must exist", "User can't be blank", "Content is too long (maximum is 140 characters)"]

13.1.3 演習 User/Micropostの関連付け

13.1.3.1

<問題>データベースにいる最初のユーザーを変数userに代入してください。そのuserオブジェクトを使ってmicropost = user.microposts.create(content: "Lorem ipsum")を実行すると、どのような結果が得られるでしょうか?

<回答>

> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x007fbc06359de0
 id: 1,
 name: "Example User",
 email: "example@railstutorial.org",
 created_at: Wed, 30 Aug 2017 01:05:28 UTC +00:00,
 updated_at: Wed, 30 Aug 2017 01:05:28 UTC +00:00,
 password_digest: "$2a$10$zjV8yVOjS/lZaq6t2I0Ae.xOrbyTAE/G18QeHKZw/72vRIFOOaSl6",
 remember_digest: nil,
 admin: true,
 activation_digest: "$2a$10$WVoTuhmujpxjL.agLrTGeeZy/56retMcUz2fYMe.8YWfBPf.8ZnRq",
 activated: true,
 activated_at: Wed, 30 Aug 2017 01:05:28 UTC +00:00,
 reset_digest: nil,
 reset_sent_at: nil>
[30] pry(main)> micropost = user.microposts.create(content: "Lorem ipsum")
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "Lorem ipsum"], ["user_id", 1], ["created_at", 2017-08-30 01:40:35 UTC], ["updated_at", 2017-08-30 01:40:35 UTC]]
   (2.5ms)  commit transaction
=> #<Micropost:0x007fbc06409380 id: 302, content: "Lorem ipsum", user_id: 1, created_at: Wed, 30 Aug 2017 01:40:35 UTC +00:00, updated_at: Wed, 30 Aug 2017 01:40:35 UTC +00:00, picture: nil>

13.1.3.2

<問題>先ほどの演習課題で、データベース上に新しいマイクロポストが追加されたはずです。user.microposts.find(micropost.id)を実行して、本当に追加されたのかを確かめてみましょう。また、先ほど実行したmicropost.idの部分をmicropostに変更すると、結果はどうなるでしょうか?

<回答>

> user.microposts.find(micropost.id)
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ?  [["user_id", 1], ["id", 302], ["LIMIT", 1]]
=> #<Micropost:0x007fbc05d60240 id: 302, content: "Lorem ipsum", user_id: 1, created_at: Wed, 30 Aug 2017 01:40:35 UTC +00:00, updated_at: Wed, 30 Aug 2017 01:40:35 UTC +00:00, picture: nil>

[43] pry(main)> user.microposts.find(micropost)
DEPRECATION WARNING: You are passing an instance of ActiveRecord::Base to `find`. Please pass the id of the object by calling `.id`. (called from <main> at (pry):18)
  Micropost Load (0.1ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ?  [["user_id", 1], ["id", 302], ["LIMIT", 1]]
=> #<Micropost:0x007fbc051de5c0 id: 302, content: "Lorem ipsum", user_id: 1, created_at: Wed, 30 Aug 2017 01:40:35 UTC +00:00, updated_at: Wed, 30 Aug 2017 01:40:35 UTC +00:00, picture: nil>

13.1.3.3

<問題>user == micropost.userを実行した結果はどうなるでしょうか? また、user.microposts.first == micropost を実行した結果はどうなるでしょうか? それぞれ確認してみてください。

<回答>

> user == micropost.user
=> true
[47] pry(main)> user.microposts.first == micropost
=> true

13.1.4 演習 マイクロポストを改良する

13.1.4.1

<問題>Micropost.first.created_atの実行結果と、Micropost.last.created_atの実行結果を比べてみましょう。

<回答>

> Micropost.first.created_at.to_s
  Micropost Load (0.4ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
=> "2017-08-30 01:40:35 UTC"
[14] pry(main)> Micropost.last.created_at.to_s
  Micropost Load (0.4ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ?  [["LIMIT", 1]]
=> "2017-08-30 01:05:42 UTC"

あー僕これ勘違いしてた。firstで取得した時はid順で取ってくると思ってた。 default_scope設定してるんだから、そりゃー降順で取ってくるわな。 きちんと発行されたSQL見るのは大事ですね。

13.1.4.2

<問題>Micropost.firstを実行したときに発行されるSQL文はどうなっているでしょうか? 同様にして、Micropost.lastの場合はどうなっているでしょうか? ヒント: それぞれをコンソール上で実行したときに表示される文字列が、SQL文になります。

<回答>

> Micropost.first
  Micropost Load (0.4ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
[19] pry(main)> Micropost.last
  Micropost Load (0.4ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ?  [["LIMIT", 1]]

firstは、DESC。lastは、ASCで発行されていることが分かる。

13.1.4.3

<問題>データベース上の最初のユーザーを変数userに代入してください。そのuserオブジェクトが最初に投稿したマイクロポストのidはいくつでしょうか? 次に、destroyメソッドを使ってそのuserオブジェクトを削除してみてください。削除すると、そのuserに紐付いていたマイクロポストも削除されていることをMicropost.findで確認してみましょう。

<回答>

# 最初に投稿したマイクロポスト
> user.microposts.last
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" ASC LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
=> #<Micropost:0x007fbc00dda3b0
 id: 1,
 content: "Cum consequatur enim quibusdam aliquid corrupti reprehenderit et.",
 user_id: 1,
 created_at: Wed, 30 Aug 2017 01:05:42 UTC +00:00,
 updated_at: Wed, 30 Aug 2017 01:05:42 UTC +00:00,
 picture: nil>

# ユーザーをdestroy
> user.destroy
   (0.1ms)  begin transaction
  Micropost Load (0.6ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC  [["user_id", 1]]
  SQL (0.3ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 302]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 301]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 295]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 289]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 283]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 277]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 271]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 265]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 259]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 253]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 247]]
  SQL (0.2ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 241]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 235]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 229]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 223]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 217]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 211]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 205]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 199]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 193]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 187]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 181]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 175]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 169]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 163]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 157]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 151]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 145]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 139]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 133]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 127]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 121]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 115]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 109]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 103]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 97]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 91]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 85]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 79]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 73]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 67]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 61]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 55]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 49]]
  SQL (0.0ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 43]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 37]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 31]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 25]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 19]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 13]]
  SQL (0.2ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 7]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 1]]
  SQL (0.2ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 1]]
   (3.1ms)  commit transaction
=> #<User:0x007fbc04851110
 id: 1,
 name: "Example User",
 email: "example@railstutorial.org",
 created_at: Wed, 30 Aug 2017 01:05:28 UTC +00:00,
 updated_at: Wed, 30 Aug 2017 01:05:28 UTC +00:00,
 password_digest: "$2a$10$zjV8yVOjS/lZaq6t2I0Ae.xOrbyTAE/G18QeHKZw/72vRIFOOaSl6",
 remember_digest: nil,
 admin: true,
 activation_digest: "$2a$10$WVoTuhmujpxjL.agLrTGeeZy/56retMcUz2fYMe.8YWfBPf.8ZnRq",
 activated: true,
 activated_at: Wed, 30 Aug 2017 01:05:28 UTC +00:00,
 reset_digest: nil,
 reset_sent_at: nil>

> Micropost.find(1)
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ?  [["id", 1], ["LIMIT", 1]]
ActiveRecord::RecordNotFound: Couldn't find Micropost with 'id'=1

> user.find(1)
NoMethodError: undefined method `find' for #<User:0x007fbc04851110>

13.2.1 演習 マイクロポストの描画

13.2.1.1

<問題>7.3.3で軽く説明したように、今回ヘルパーメソッドとして使ったtime_ago_in_wordsメソッドは、Railsコンソールのhelperオブジェクトから呼び出すことができます。このhelperオブジェクトのtime_ago_in_wordsメソッドを使って、3.weeks.agoや6.months.agoを実行してみましょう。

<回答>

> helper.time_ago_in_words(3.weeks.ago)
=> "21 days"
[48] pry(main)> helper.time_ago_in_words(6.months.ago)
=> "6 months"

13.2.1.2

<問題>helper.time_ago_in_words(1.year.ago)と実行すると、どういった結果が返ってくるでしょうか?

<回答>

> helper.time_ago_in_words(1.year.ago)
=> "about 1 year"

13.2.1.3

<問題>micropostsオブジェクトのクラスは何でしょうか? ヒント: リスト 13.23内のコードににあるように、まずはpaginateメソッド (引数はpage: nil) でオブジェクトを取得し、その後classメソッドを呼び出してみましょう。

<回答>

> user = User.first
> microposts = user.microposts.paginate(page: 1)
> microposts.class
=> Micropost::ActiveRecord_AssociationRelation

13.2.2 演習 マイクロポストのサンプル

13.2.2.1

<問題>(1..10).to_a.take(6)というコードの実行結果を推測できますか? 推測した値が合っているかどうか、実際にコンソールを使って確認してみましょう。

<回答>

> (1..10).to_a.take(6)
=> [1, 2, 3, 4, 5, 6]

13.2.2.2

<問題>先ほどの演習にあったto_aメソッドの部分は本当に必要でしょうか? 確かめてみてください。

<回答>

> (1..10).take(6)
=> [1, 2, 3, 4, 5, 6]

> (1..10).class
=> Range
[72] pry(main)> (1..10).to_a.class
=> Array

13.2.2.3

<問題>Fakerはlorem ipsum以外にも、非常に多種多様の事例に対応しています。Fakerのドキュメント (英語) を眺めながら画面に出力する方法を学び、実際に大学名や電話番号、Hipster IpsumやChuck Norris facts (参考: チャック・ノリスの真実) を画面に出力してみましょう。(訳注: もちろん日本語にも対応していて、例えば沖縄らしい用語を出力するfaker-okinawaもあります。ぜひ遊んでみてください。)

<回答>

> Faker::Cat.name
=> "Shadow"

> Faker::SlackEmoji.people
=> ":stuck_out_tongue_winking_eye:"

> Faker::Music.key
=> "Fb"

13.2.3 演習 プロフィール画面のマイクロポストをテストする

13.2.3.1

<問題>リスト 13.28にある2つの’h1’のテストが正しいか確かめるため、該当するアプリケーション側のコードをコメントアウトしてみましょう。テストが green から redに変わることを確認してみてください。

<回答>

# app/views/users/show.html.erb
5      <!-- <h1>
6        <%= gravatar_for @user %>
7        <%= @user.name %>
8      </h1> -->

13.2.3.2

<問題>リスト 13.28にあるテストを変更して、will_paginateが1度のみ表示されていることをテストしてみましょう。ヒント: 表 5.2を参考にしてください。

<回答>

  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', count: 1 # ここを修正。
    @user.microposts.paginate(page: 1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end

13.3.1 演習 マイクロポストのアクセス制御

13.3.1.1

<問題>なぜUsersコントローラ内にあるlogged_in_userフィルターを残したままにするとマズイのでしょうか? 考えてみてください。

<回答> コードが重複してしまうため

13.3.2 演習 マイクロポストを作成する

13.3.2.1

<問題>Homeページをリファクタリングして、if-else文の分岐のそれぞれに対してパーシャルを作ってみましょう。

<回答>

# app/views/static_pages/home.html.erb
<% if logged_in? %>
  <%= render 'user_logged_in' %>
<% else %>
  <%= render 'user_logged_in' %>
<% end %>
# app/views/static_pages/_user_logged_in.html.erb
<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>
# app/views/static_pages/_user_not_logged_in.html.erb
<% provide(:title, "Home") %>
<div class="center jumbotron">
  <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-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.png", alt: "Rails logo"), "https://rubyonrails.org/" %>

13.3.3 演習 フィードの原型

13.3.3.1

<問題>新しく実装したマイクロポストの投稿フォームを使って、実際にマイクロポストを投稿してみましょう。Railsサーバーのログ内にあるINSERT文では、どういった内容をデータベースに送っているでしょうか? 確認してみてください。

<回答>

INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "hakuの休日"], ["user_id", 101], ["created_at", 2017-08-30 05:01:10 UTC], ["updated_at", 2017-08-30 05:01:10 UTC]]

13.3.3.2

<問題>コンソールを開き、user変数にデータベース上の最初のユーザーを代入してみましょう。その後、Micropost.where("user_id = ?", user.id)とuser.microposts、そしてuser.feedをそれぞれ実行してみて、実行結果がすべて同じであることを確認してみてください。ヒント: ==で比較すると結果が同じかどうか簡単に判別できます。

<回答>

> user.feed
  Micropost Load (0.4ms)  SELECT "microposts".* FROM "microposts" WHERE (user_id=2) ORDER BY "microposts"."created_at" DESC

> Micropost.where("user_id = ?", user.id)
  Micropost Load (0.4ms)  SELECT "microposts".* FROM "microposts" WHERE (user_id = 2) ORDER BY "microposts"."created_at" DESC

> user.feed == user.microposts
  Micropost Load (0.4ms)  SELECT "microposts".* FROM "microposts" WHERE (user_id=2) ORDER BY "microposts"."created_at" DESC
=> true
[57] pry(main)> Micropost.where("user_id = ?", user.id) == user.microposts
  Micropost Load (0.4ms)  SELECT "microposts".* FROM "microposts" WHERE (user_id = 2) ORDER BY "microposts"."created_at" DESC
=> true

# なぜか`false`になる。発行されているSQLも一緒なのに。なぜだ?
> Micropost.where("user_id = ?", user.id) == user.feed
=> false

13.3.4 演習 マイクロポストを削除する

13.3.4.1

<問題>マイクロポストを作成し、その後、作成したマイクロポストを削除してみましょう。次に、Railsサーバーのログを見てみて、DELETE文の内容を確認してみてください。

<回答>

DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 303]]

13.3.4.2

<問題>redirect_to request.referrer || root_urlの行をredirect_back(fallback_location: root_url)と置き換えてもうまく動くことを、ブラウザを使って確認してみましょう (このメソッドはRails 5から新たに導入されました)。

<回答> エラーなく削除できることを確認した。

13.3.5 演習 フィード画面のマイクロポストをテストする

13.3.5.1

<問題>リスト 13.55で示した4つのコメント (「無効な送信」など) のそれぞれに対して、テストが正しく動いているか確認してみましょう。具体的には、対応するアプリケーション側のコードをコメントアウトし、テストが redになることを確認し、元に戻すと greenになることを確認してみましょう。

<回答>

くそー動かない。画面でも再現する。

ERROR["test_micropost_interface", MicropostsInterfaceTest, 3.208587193999847]
 test_micropost_interface#MicropostsInterfaceTest (3.21s)
ActionView::Template::Error:         ActionView::Template::Error: Missing partial microposts/_user_logged_in, application/_user_logged_in with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]}. Searched in:
          * "/Users/taku/rails/railstutorial/railstutorial_13/app/views"

            app/views/static_pages/home.html.erb:2:in `_app_views_static_pages_home_html_erb___3536680938325879454_70206177007920'
            app/controllers/microposts_controller.rb:12:in `create'
            test/integration/microposts_interface_test.rb:16:in `block (2 levels) in <class:MicropostsInterfaceTest>'
            test/integration/microposts_interface_test.rb:15:in `block in <class:MicropostsInterfaceTest>'

解決した!

# app/views/static_pages/home.html.erb
<% if logged_in? %>
  <%= render 'static_pages/user_logged_in' %> # `user_logged_in`だけ書いてたのが原因。同じディレクトリでもフォルダ名から書く!
<% else %>
  <%= render 'static_pages/user_not_logged_in' %> # `user_logged_in`だけ書いてたのが原因。同じディレクトリでもフォルダ名から書く!
<% end %>

13.3.5.2

<問題>サイドバーにあるマイクロポストの合計投稿数をテストしてみましょう。このとき、単数形 (micropost) と複数形 (microposts) が正しく表示されているかどうかもテストしてください。ヒント: リスト 13.57を参考にしてみてください。

<回答>

# test/integration/microposts_interface_test.rb
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 "#{@user.microposts.count} microposts", response.body # ここを修正
    # まだマイクロポストを投稿していないユーザー
    other_user = users(:malory)
    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 "1 micropost", response.body # ここを修正
  end
end

13.4.1 演習 基本的な画像アップロード

13.4.1.1

<問題>画像付きのマイクロポストを投稿してみましょう。もしかして、大きすぎる画像が表示されてしまいましたか? (心配しないでください、次の13.4.3でこの問題を直します)。

<回答> 問題なくアップロードできる!

13.4.1.2

<問題>リスト 13.63に示すテンプレートを参考に、13.4で実装した画像アップローダーをテストしてください。テストの準備として、まずはサンプル画像をfixtureディレクトリに追加してください (コマンド例: cp app/assets/images/rails.png test/fixtures/)。リスト 13.63で追加したテストでは、Homeページにあるファイルアップロードと、投稿に成功した時に画像が表示されているかどうかをチェックしています。なお、テスト内にあるfixture_file_uploadというメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです18。ヒント: picture属性が有効かどうかを確かめるときは、11.3.3で紹介したassignsメソッドを使ってください。このメソッドを使うと、投稿に成功した後にcreateアクション内のマイクロポストにアクセスするようになります。

<回答>

# test/integration/microposts_interface_test.rb
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_match "#{@user.microposts.count} microposts", response.body
    assert_select "div.pagination"
    assert_select 'input[type=file]'
    # 無効な送信
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: {micropost: {content: ""}}
    end
    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, params: {micropost: {content: content, picture: picture}}
    end
    assert assigns(:micropost).picture?
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    assert_match "#{@user.microposts.count} micropost", 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

エラーになるので、改めて確認する。

13.4.2 演習 画像の検証

13.4.2.1

<問題>5MB以上の画像ファイルを送信しようとした場合、どうなりますか?

<回答> メッセージウィンドウが表示される(Maximum file size is 5MB. Please choose a smaller file.)

13.4.2.1

<問題>無効な拡張子のファイルを送信しようとした場合、どうなりますか?

<回答>

Picture translation missing: en.errors.messages.extension_whitelist_error

13.4.3 演習 画像のリサイズ

13.4.3.1

<問題>解像度の高い画像をアップロードし、リサイズされているかどうか確認してみましょう。画像が長方形だった場合、リサイズはうまく行われているでしょうか?

<回答> 問題なし

13.4.3.2

<問題>既にリスト 13.63のテストを追加していた場合、この時点でテストスイートを走らせるとエラーメッセージが表示されるようになるはずです。このエラーを取り除いてみましょう。ヒント: リスト 13.68にある設定ファイルを修正し、テスト時はCarrierWaveに画像のリサイズをさせないようにしてみましょう。

<回答> - エラーにならない - config/initializers/skip_image_resizing.rbが存在しない

Ruby on Rails チュートリアルで30歳までに人生を変える(第12章)

こんにちは。opiyoです。

今回は、第12章をやっていきます。

第12章はパスワードを忘れた時の再設定方法です。

どうやら難しそうですが、早速行ってみましょう。

railsチュートリアル12章の演習解説

12.1.1 演習 PasswordResetsコントローラ

12.1.1.1

<問題>この時点で、テストスイートが greenになっていることを確認してみましょう。

<回答> 問題なし

12.1.1.2

<問題>表 12.1の名前付きルートでは、pathではなくurlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: アカウント有効化で行った演習 (11.1.1.1) と同じ理由です。

<回答> メールの本文で使うリンクになるので、絶対パス=_urlを使わないとどのサイトなのか参照できないから

12.1.2 演習 新しいパスワードの設定

12.1.2.1

<問題>リスト 12.4のform_forメソッドでは、なぜ@password_resetではなく:password_resetを使っているのでしょうか? 考えてみてください。

<回答> @password_reset(オブジェクト)を使う必要がない。これを使うとオブジェクトの有り・無しによってRailsがよしなにやってくれる。 今回は、入力されたパスワードを使ってDB検索するだけなので一番シンプルな形にするために:password_rest(シンボル)を使ってる。

12.1.3 演習 createアクションでパスワード再設定

12.1.3.1

<問題>試しに有効なメールアドレスをフォームから送信してみましょう (図 12.6)。どんなエラーメッセージが表示されたでしょうか?

<回答> エラーにならない。

12.1.3.2

<問題>コンソールに移り、先ほどの演習課題で送信した結果、(エラーと表示されてはいるものの) 該当するuserオブジェクトにはreset_digestとreset_sent_atがあることを確認してみましょう。また、それぞれの値はどのようになっていますか?

<回答> エラーにならなかったので、再度確認します。

12.2.1 演習 新しいパスワードの設定

12.2.1.1

<問題>ブラウザから、送信メールのプレビューをしてみましょう。「Date」の欄にはどんな情報が表示されているでしょうか?

<回答> Date:Mon, 28 Aug 2017 07:09:54 +0000

12.2.1.2

<問題>パスワード再設定フォームから有効なメールアドレスを送信してみましょう。また、Railsサーバーのログを見て、生成された送信メールの内容を確認してみてください。

<回答> メッセージが表示されエラーなく登録された(Password has been reset.)

Sent mail to haku@co.jp (18.8ms)
Date: Mon, 28 Aug 2017 16:10:44 +0900
From: noreply@example.com
To: haku@co.jp
Message-ID: <59a3c1f4ad18e_8d93fdb09687134926e4@rh0257.rhizomedom.co.jp.mail>
Subject: Password reset
Mime-Version: 1.0
Content-Type: multipart/alternative;
 boundary="--==_mimepart_59a3c1f4ac5a4_8d93fdb096871349250";
 charset=UTF-8
Content-Transfer-Encoding: 7bit


----==_mimepart_59a3c1f4ac5a4_8d93fdb096871349250
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

To reset your password click the link below:

http://localhost:3000/password_resets/6rzf5BbQ9uFRCrekZp3PtA/edit?email=haku%40co.jp

This link will expire in two hours.

If you did not request your password to be reset, please ignore this email and
your password will stay as it is.


----==_mimepart_59a3c1f4ac5a4_8d93fdb096871349250
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <h1>Password reset</h1>

<p>To reset your password click the link below:</p>

<a href="http://localhost:3000/password_resets/6rzf5BbQ9uFRCrekZp3PtA/edit?email=haku%40co.jp">Reset password</a>

<p>This link will expire in two hours.</p>

<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>

  </body>
</html>

----==_mimepart_59a3c1f4ac5a4_8d93fdb096871349250--

12.2.1.3

<問題>コンソールに移り、先ほどの演習課題でパスワード再設定をしたUserオブジェクトを探してください。オブジェクトを見つけたら、そのオブジェクトが持つreset_digestとreset_sent_atの値を確認してみましょう。

<回答>

> User.last
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x007fc463209b10
 id: 102,
 name: "haku",
 email: "haku@co.jp",
 created_at: Mon, 28 Aug 2017 03:26:44 UTC +00:00,
 updated_at: Mon, 28 Aug 2017 07:11:03 UTC +00:00,
 password_digest: "$2a$10$BGL4JzqH6XKZQIHcaZG1CeAXndlo25Gaj1z.74gDwTTHEwjlzuZ9a",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$kSWicCWAUJWf8ip4/LKhVehAvTG.t56.W2PyXWfVykUkpM6wzTHTa",
 activated: true,
 activated_at: Mon, 28 Aug 2017 03:26:47 UTC +00:00,
 reset_digest: "$2a$10$SjkLM3hWhvqIGxWzmXHftOtINDC89Ti9OZQrJBpMq2jyFIB2.fzoG",
 reset_sent_at: Mon, 28 Aug 2017 07:10:44 UTC +00:00>

12.2.2 演習 送信メールのテスト

12.2.2.1

<問題>メイラーのテストだけを実行してみてください。このテストは greenになっているでしょうか?

<回答> なっている。

12.2.2.2

<問題>リスト 12.12にある2つ目のCGI.escapeを削除すると、テストが redになることを確認してみましょう。

<回答>

FAIL["test_password_reset", UserMailerTest, 2.998306857998614]
 test_password_reset#UserMailerTest (3.00s)
        Expected /michael@example\.com/ to match # encoding: US-ASCII
        "\r\n----==_mimepart_59a3c6f85e58_f443ffb6443fa0465c8\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nTo reset your password click the link below:\r\n\r\nhttp://localhost:3000/password_resets/95ZsztFc2ioqiuI92qDcxQ/edit?email=michael%40example.com\r\n\r\nThis link will expire in two hours.\r\n\r\nIf you did not request your password to be reset, please ignore this email and\r\nyour password will stay as it is.\r\n\r\n\r\n----==_mimepart_59a3c6f85e58_f443ffb6443fa0465c8\r\nContent-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n<!DOCTYPE html>\r\n<html>\r\n  <head>\r\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\r\n    <style>\r\n      /* Email styles need to be inline */\r\n    </style>\r\n  </head>\r\n\r\n  <body>\r\n    <h1>Password reset</h1>\r\n\r\n<p>To reset your password click the link below:</p>\r\n\r\n<a href=\"http://localhost:3000/password_resets/95ZsztFc2ioqiuI92qDcxQ/edit?email=michael%40example.com\">Reset password</a>\r\n\r\n<p>This link will expire in two hours.</p>\r\n\r\n<p>\r\nIf you did not request your password to be reset, please ignore this email and\r\nyour password will stay as it is.\r\n</p>\r\n\r\n  </body>\r\n</html>\r\n\r\n----==_mimepart_59a3c6f85e58_f443ffb6443fa0465c8--\r\n".
        test/mailers/user_mailer_test.rb:26:in `block in <class:UserMailerTest>'

12.3.1 演習 editアクションで再設

12.3.1.1

<問題>12.2.1.1で示した手順に従って、Railsサーバーのログから送信メールを探し出し、そこに記されているリンクを見つけてください。そのリンクをブラウザから表示してみて、図 12.11のように表示されるか確かめてみましょう。

<回答> http://localhost:3000/password_resets/-e_N9zdv0kKekwluwQXnzw/edit?email=haku%40co.jp 表示させることを確認。

12.3.1.2

<問題>先ほど表示したページから、実際に新しいパスワードを送信してみましょう。どのような結果になるでしょうか?

<回答> エラーにならないので、再度確認する

12.3.2 演習 パスワードを更新する

12.3.2.1

<問題>12.2.1.1で得られたリンク (Railsサーバーのログから取得) をブラウザで表示し、passwordとconfirmationの文字列をわざと間違えて送信してみましょう。どんなエラーメッセージが表示されるでしょうか?

<回答>

The form contains 1 error.
Password confirmation doesn't match Password
["Password confirmation doesn't match Password"]

12.3.3.2

<問題>コンソールに移り、パスワード再設定を送信したユーザーオブジェクトを見つけてください。見つかったら、そのオブジェクトのpassword_digestの値を取得してみましょう。次に、パスワード再設定フォームから有効なパスワードを入力し、送信してみましょう (図 12.13)。パスワードの再設定は成功したら、再度password_digestの値を取得し、先ほど取得した値と異なっていることを確認してみましょう。ヒント: 新しい値はuser.reloadを通して取得する必要があります。

<回答>

> after = User.last
# パスワード変更を実施
> before = User.last
> before.password_digest == after.password_digest
=> false

12.3.3 演習 パスワードを更新する

12.3.3.1

<問題>リスト 12.6にあるcreate_reset_digestメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 12.20に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう (これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 greenになることも確認してください。ちなみにリスト 12.20にあるコードには、前章の演習 (リスト 11.39) の解答も含まれています。

<回答>

# app/models/user.rb
  def create_reset_digest
    self.reset_token = User.new_token
    update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
  end

12.3.3.2

<問題>リスト 12.16のテンプレートを埋めて、期限切れのパスワード再設定で発生する分岐 (リスト 12.21) を統合テストで網羅してみましょう (12.21 のコードにあるresponse.bodyは、そのページのHTML本文をすべて返すメソッドです)。期限切れをテストする方法はいくつかありますが、リスト 12.21でオススメした手法を使えば、レスポンスの本文に「expired」という語があるかどうかでチェックできます (なお、大文字と小文字は区別されません)。

12.3.3.3

<問題>2時間経ったらパスワードを再設定できなくする方針は、セキュリティ的に好ましいやり方でしょう。しかし、もっと良くする方法はまだあります。例えば、公共の (または共有された) コンピューターでパスワード再設定が行われた場合を考えてみてください。仮にログアウトして離席したとしても、2時間以内であれば、そのコンピューターの履歴からパスワード再設定フォームを表示させ、パスワードを更新してしまうことができてしまいます (しかもそのままログイン機構まで突破されてしまいます!)。この問題を解決するために、リスト 12.22のコードを追加し、パスワードの再設定に成功したらダイジェストをnilになるように変更してみましょう5。

12.3.3.4

<問題>リスト 12.18に1行追加し、1つ前の演習課題に対するテストを書いてみましょう。ヒント: リスト 9.25のassert_nilメソッドとリスト 11.33のuser.reloadメソッドを組み合わせて、reset_digest属性を直接テストしてみましょう。

Ruby on Rails チュートリアルで30歳までに人生を変える(第11章)

こんにちは。opiyoです。

今回は、第11章をやっていきます。

第11章はメールを使ってアカウントを有効化する方法です。

どうやら難しそうですが、早速行ってみましょう。

railsチュートリアル11章の演習解説

11.1.1 演習 AccountActivationsコントローラ

11.1.1.1

<問題>現時点でテストスイートを実行すると greenになることを確認してみましょう。

<回答> 問題ないことを確認

11.1.1.1

<問題>表 11.2の名前付きルートでは、pathではなくurlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。

<回答> _pathだと、相対パスでの表記になってしまうため。

11.1.2 演習 AccountActivationのデータモデル

11.1.2.1

<問題>本項での変更を加えた後、テストスイートが green のままになっていることを確認してみましょう。

<回答> 問題ないことを確認

11.1.2.2

<問題>コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると (Privateメソッドなので) NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。

<回答>

> u = User.last
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x007fcb594c0f50
 id: 101,
 name: "haku",
 email: "haku@co.jp",
 created_at: Mon, 07 Aug 2017 03:43:38 UTC +00:00,
 updated_at: Mon, 07 Aug 2017 03:43:38 UTC +00:00,
 password_digest: "$2a$10$A3jl3TpJQRtjO4ENd.vAA.8X.hpGb0v/NpDjcBmvHXw0HdGZGN5h6",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$PY8DTSnaULLELaVLIF/agOae7WOsh/gZFfS0U9cGYBRQnCqQR3vby",
 activated: false,
 activated_at: nil>
[22] pry(main)> u.create_activation_digest
NoMethodError: private method `create_activation_digest' called for #<User:0x007fcb594c0f50>
Did you mean?  created_at_change
               created_at_previous_change
               created_at
               created_at_was
               created_at_changed?
               created_at_will_change!
               restore_activation_digest!
from /Users/taku/.rbenv/versions/2.3.4/lib/ruby/gems/2.3.0/gems/activemodel-5.0.0.1/lib/active_model/attribute_methods.rb:430:in `method_missing'
[23] pry(main)> u.activation_digest
=> "$2a$10$PY8DTSnaULLELaVLIF/agOae7WOsh/gZFfS0U9cGYBRQnCqQR3vby"

11.1.2.3

<問題>リスト 6.34で、メールアドレスの小文字化にはemail.downcase!という (代入せずに済む) メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。

<回答>

class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save { email.downcase! }

  private

  # def downcase_email
  #   self.email = email.downcase
  # end

end

11.2.1 演習 送信メールのテンプレート

11.2.1.1

<問題>コンソールを開き、CGIモジュールのescapeメソッド (リスト 11.15) でメールアドレスの文字列をエスケープできることを確認してみましょう。このメソッドで"Don’t panic!"をエスケープすると、どんな結果になりますか?

<回答>

> CGI.escape('foo@example.com')
=> "foo%40example.com"
[5] pry(main)> CGI.escape("Don’t panic!")
=> "Don%E2%80%99t+panic%21"
> CGI.escape("!#$%&'()0=~|`{+*}<>?_")
=> "%21%23%24%25%26%27%28%290%3D%7E%7C%60%7B%2B%2A%7D%3C%3E%3F_"

11.2.2 演習 送信メールのプレビュー

11.2.2.1

<問題>Railsプレビュー機能を使って、ブラウザから先ほどのメールを表示してみてください。「Date」の欄にはどんな内容が表示されているでしょうか?

<回答>

Date: Fri, 25 Aug 2017 08:35:47 +0000

http://localhost:3000/rails/mailers/user_mailer/account_activationをブラウザで表示した時間が表示される。

11.2.3 演習 送信メールのテスト

11.2.3.1

<問題>この時点で、テストスイートが greenになっていることを確認してみましょう。

<回答> 問題ないことを確認した。

<問題>リスト 11.20で使ったCGI.escapeの部分を削除すると、テストが redに変わることを確認してみましょう。

<回答>

 FAIL["test_account_activation", UserMailerTest, 5.321151973999804]
 test_account_activation#UserMailerTest (5.32s)
        Expected /michael@example\.com/ to match # encoding: US-ASCII
        "\r\n----==_mimepart_599fe73745950_18423fe6edc3fa14170d5\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHi Michael Example,\r\n\r\nWelcome to the Sample App! Click on the link below to activate your account:\r\n\r\nhttp://localhost:3000/account_activations/8LjBfLrHo_srJ_DuuIXlRw/edit?email=michael%40example.com\r\n\r\n\r\n----==_mimepart_599fe73745950_18423fe6edc3fa14170d5\r\nContent-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n<!DOCTYPE html>\r\n<html>\r\n  <head>\r\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\r\n    <style>\r\n      /* Email styles need to be inline */\r\n    </style>\r\n  </head>\r\n\r\n  <body>\r\n    <h1>Sample App</h1>\r\n\r\n<p>Hi Michael Example,</p>\r\n\r\n<p>\r\nWelcome to the Sample App! Click on the link below to activate your account:\r\n</p>\r\n\r\n<a href=\"http://localhost:3000/account_activations/8LjBfLrHo_srJ_DuuIXlRw/edit?email=michael%40example.com\">Activate</a>\r\n\r\n  </body>\r\n</html>\r\n\r\n----==_mimepart_599fe73745950_18423fe6edc3fa14170d5--\r\n".
        test/mailers/user_mailer_test.rb:15:in `block in <class:UserMailerTest>'
(byebug) user.email
"michael@example.com" # `@`がそのまま表示される!
(byebug) CGI.escape(user.email)
"michael%40example.com"

11.2.4 演習 ユーザーのcreateアクションを更新

11.2.4.1

<問題>新しいユーザーを登録したとき、リダイレクト先が適切なURLに変わったことを確認してみましょう。その後、Railsサーバーのログから送信メールの内容を確認してみてください。有効化トークンの値はどうなっていますか?

<回答> - http://localhost:3000/にリダイレクトされることを確認。 - <a href="http://localhost:3000/account_activations/GworqOheQT8myuVYkpf3hw/edit?email=taku%40co.jp">Activate</a>

<問題>コンソールを開き、データベース上にユーザーが作成されたことを確認してみましょう。また、このユーザーはデータベース上にはいますが、有効化のステータスがfalseのままになっていることを確認してください。

<回答>

> User.last
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x007fcb535b1cd0
 id: 104,
 name: "taku",
 email: "taku@co.jp",
 created_at: Fri, 25 Aug 2017 09:22:39 UTC +00:00,
 updated_at: Fri, 25 Aug 2017 09:22:39 UTC +00:00,
 password_digest: "$2a$10$9Txlyza5j1zT5lmL1ldaJ.5ukrmPGHDYsuF6xCSDB6UJ0AdTPbqn.",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$k8NN5m1heGxd9K5t4lsd8OI2UHeTMSVhZcyllq7K9sIDpQQJYDgDu", # トークンは設定されている
 activated: false, # falseになってる
 activated_at: nil>

11.3.1 演習 authenticated?メソッドの抽象化

11.3.1.1

<問題>コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?

<回答>

> User.create!(name:  "13-1",
[2] pry(main)*   email: "13-1@railstutorial.org",
[2] pry(main)*   password:              "foobar",
[2] pry(main)*   password_confirmation: "foobar",
[2] pry(main)*   admin: true,
[2] pry(main)*   activated: true,
[2] pry(main)* activated_at: Time.zone.now)
   (0.1ms)  begin transaction
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "13-1@railstutorial.org"], ["LIMIT", 1]]
  SQL (0.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "admin", "activation_digest", "activated", "activated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)  [["name", "13-1"], ["email", "13-1@railstutorial.org"], ["created_at", 2017-08-25 09:39:28 UTC], ["updated_at", 2017-08-25 09:39:28 UTC], ["password_digest", "$2a$10$60JFHnETt90jy9lIclanOuEwrBLJcJUd71B.iWjMYHrXE/v5VylLy"], ["admin", true], ["activation_digest", "$2a$10$BQV6wUIYXaaVlZkuIcC8HOYD0qllFBohGUBM9Ib3jmAhfVMRMVqMe"], ["activated", true], ["activated_at", 2017-08-25 09:39:27 UTC]]
   (2.7ms)  commit transaction
=> #<User:0x007fcb546f9ce0
 id: 105,
 name: "13-1",
 email: "13-1@railstutorial.org",
 created_at: Fri, 25 Aug 2017 09:39:28 UTC +00:00,
 updated_at: Fri, 25 Aug 2017 09:39:28 UTC +00:00,
 password_digest: "$2a$10$60JFHnETt90jy9lIclanOuEwrBLJcJUd71B.iWjMYHrXE/v5VylLy",
 remember_digest: nil,
 admin: true,
 activation_digest: "$2a$10$BQV6wUIYXaaVlZkuIcC8HOYD0qllFBohGUBM9Ib3jmAhfVMRMVqMe",
 activated: true,
 activated_at: Fri, 25 Aug 2017 09:39:27 UTC +00:00>

<問題>リスト 11.26で抽象化したauthenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。

<回答> 問題ないことを確認した

11.3.2 演習 editアクションで有効化

11.3.2.1

<問題>コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?

<回答>

http://localhost:3000/account_activations/466u-4hqImyNdeVsrhPbKw/edit?email=haku%40co.jp

466u-4hqImyNdeVsrhPbKwこの部分が有効化トーク

11.3.2.2

<問題>先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。また、有効化ステータスがtrueになっていることをコンソールから確認してみてください。

<回答>

# 有効化前
> User.last
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x007fa9f1ec3e78
 id: 102,
 name: "toma",
 email: "toma@co.jp",
 created_at: Sat, 26 Aug 2017 13:50:08 UTC +00:00,
 updated_at: Sat, 26 Aug 2017 13:50:08 UTC +00:00,
 password_digest: "$2a$10$A6P5nYGWenFVG6b23WZbi.G8GFHj7HZi7YfsDxoT0C/FMi/86QoZ6",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$cIGjCz5OVXBxeJ3QUC56ouStDhT6InDg087d6BaKh2A0/zGlh6sLK",
 activated: false,
# 有効化後
[14] pry(main)> User.last
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x007fa9f1d462a8
 id: 102,
 name: "toma",
 email: "toma@co.jp",
 created_at: Sat, 26 Aug 2017 13:50:08 UTC +00:00,
 updated_at: Sat, 26 Aug 2017 13:50:18 UTC +00:00,
 password_digest: "$2a$10$A6P5nYGWenFVG6b23WZbi.G8GFHj7HZi7YfsDxoT0C/FMi/86QoZ6",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$cIGjCz5OVXBxeJ3QUC56ouStDhT6InDg087d6BaKh2A0/zGlh6sLK",
 activated: true,
 activated_at: Sat, 26 Aug 2017 13:50:18 UTC +00:00>

11.3.3 演習 有効化のテストとリファクタリング

11.3.3.1

<問題>リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう (これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 greenになることも確認してください。

<回答>

# app/models/user.rb
  def activate
    update_attributes(activated: true, activated_at: Time.zone.now)
  end

テストが成功することも確認した

11.3.3.2

<問題>現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト 11.40のテンプレートを使って、この動作を変更してみましょう9。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。

<回答>

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  def index
    @users = User.where(activated: true).paginate(page: params[:page])
  end

  def show
    @user = User.find(params[:id])
    redirect_to root_url and return unless @user.activated
  end
  .
end

11.3.3.3

<問題>ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。

<回答>

# test/integration/users_index_test.rb
class UsersIndexTest < ActionDispatch::IntegrationTest

  test "index as admin including pagination and delete links" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    first_page_of_users = User.where(activated: true).paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
  end
end

11.3.3.2の修正を行うと、テストがエラーになるので有効化されたユーザーのみテスト対象となるように修正する

Ruby on Rails チュートリアルで30歳までに人生を変える(第10章)

こんにちは。opiyoです。

今回は、第10章をやっていきます。

第10章はユーザー登録以外の「表示」「編集」「削除」の方法です。

railsチュートリアル10章の学んだこと

  • target="_blank"が使われていますが、これを使うとリンク先を新しいタブ (またはウィンドウ) で開くようになる
<a href="http://gravatar.com/emails" target="_blank">change</a>
  • form_forの引数にインスタンス変数を使うと中身がある場合はそれをRailsが勝手に表示する
  • :allow_nil → trueならば、nilの検証はスキップ。つまり空文字(' ')はキャッチする!
  • beforeフィルターはコントローラ内のすべてのアクションに適用されるので、ここでは適切な:onlyオプション (ハッシュ) を渡すことで、:editと:updateアクションだけにこのフィルタが適用される
  • フレンドリーフォワーディング ... リダイレクト先は、ユーザーが開こうとしていたページにしてあげること
  • will_paginatebootstrap-will_paginate gemを使えばページネーションが作れる
  • will_paginateメソッドをviewに追加して、paginateメソッドでデータを取ってくるだけですと?
# view
<%= will_paginate %>

# controller
  def index
    @users = User.paginate(page: params[:page])
  end

-統合テスト = integration_test - $ rails generate integration_test users_index - toggleメソッド使うと反転できる?

$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true

railsチュートリアル10章の演習解説

10.1.1 演習 編集フォーム

10.1.1.1

<問題>先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング (Phising) サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel (relationship) 属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。

<回答>

# app/views/users/edit.html.erb
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
    </div>

10.1.1.2

<問題>リスト 10.5のパーシャルを使って、new.html.erbビュー (リスト 10.6) とedit.html.erbビュー (リスト 10.7) をリファクタリングしてみましょう (コードの重複を取り除いてみましょう)。ヒント: 3.4.3で使ったprovideメソッドを使うと、重複を取り除けます3。(関連するリスト 7.27の演習課題を既に解いている場合、この演習課題をうまく解けない可能性があります。うまく解けない場合は、既存のコードのどこに差異があるのか考えながらこの課題に取り組んでみましょう。例えば筆者であれば、リスト 10.5のテクニックをリスト 10.6に適用してみたり、リスト 10.7のテクニックをリスト 10.5に適用してみたりするでしょう。)

<回答>

# app/views/users/_form.html.erb
<%= form_for(@user) do |f| %>
  <%= render 'shared/error_messages', object: @user %>

  <%= f.label :name %>
  <%= f.text_field :name, class: 'form-control' %>

  <%= f.label :email %>
  <%= f.text_field :email, class: 'form-control'  %>

  <%= f.label :passwore %>
  <%= f.password_field :password, class: 'form-control'  %>

  <%= f.label :password_confirmation, "Confirmation" %>
  <%= f.password_field :password_confirmation, class: 'form-control'  %>

  <%= f.submit yield(:btn_text), class: "btn btn-primary" %>
<% end %>
# app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:btn_text, 'Create my account') %>
<h1><h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
  </div>
</div>
# app/views/users/edit.html.erb
<% provide(:title, 'Edit user') %>
<% provide(:btn_text, 'Save changes') %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
    </div>
  </div>
</div>

10.1.2 演習 編集の失敗

10.1.2.1

<問題>編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。

<回答>

The form contains 2 errors.
Email is invalid
Password is too short (minimum is 6 characters)
["Email is invalid", "Password is too short (minimum is 6 characters)"]

エラーメッセージが表示されることを確認。

10.1.3 演習 編集失敗時のテスト

10.1.3.1

<問題>リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみてましょう。ヒント: 表 5.2で紹介したassert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。

<回答>

# test/integration/users_edit_test.rb
  test "unsuccessful edit" do
    log_in_as(@user)
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "",
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }

    assert_template 'users/edit'
    assert_select "div", "The form contains 4 errors." # この行を追加した!
  end

各パラメータがvalidateに引っかかるので、4 errorsのメッセージが表示されることをチェック!

10.1.4 演習 TDDで編集を成功させる

10.1.4.1

<問題>実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。

<回答> - 問題なく更新できる。 - パスワード欄が空白で更新できる。

10.1.4.2

<問題>もしGravatarと紐付いていない適当なメールアドレス (foobar@example.comなど) に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみてましょう。

<回答>

10.2.1 演習 ユーザーにログインを要求する

10.2.1.1

<問題>デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです (結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか (テストが失敗するかどうか) 確かめてみましょう。

<回答> 問題なし

10.2.2 演習 正しいユーザーを要求する

10.2.2.1

<問題>何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。

<回答> 自身データが修正されてしまうことを保護するため。

10.2.2.2

<問題>上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?

<回答>

editアクション。

10.2.3 演習 フレンドリーフォワーディング

10.2.3.1

<問題>フレンドリーフォワーディングで、最初に渡されたURLにのみ確実に転送されていることを確認するテストを作成してみましょう。続けて、ログインを行った後、転送先のURLはデフォルト (プロフィール画面) に戻る必要もありますので、これもテストで確認してみてください。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうかを確認するテストを追加してみましょう。

<回答>

# test/integration/users_edit_test.rb
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    assert session[:forwarding_url] # sessionに値が入っていることをチェック
    assert_equal edit_user_url(@user), session[forwarding_url] # 正しい値かチェックする    
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user) # 今まではログイン後はログインユーザーのプロフィール画面に遷移してたけど、記憶したURLに遷移するようになったので、ログイン前にアクセスしたユーザー編集画面に遷移してる。
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end

10.2.3.2

<問題>7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください (デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう (デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって (コラム 1.1)、落ち着いて対処してみましょう)。

<回答>

[1, 10] in /Users/taku/rails/railstutorial/railstutorial_10/app/controllers/sessions_controller.rb
    1: class SessionsController < ApplicationController
    2:   def new
    3:     debugger
=>  4:   end
    5:
    6:   def create
    7:     user = User.find_by(email: params[:session][:email].downcase)
    8:     if user && user.authenticate(params[:session][:password])
    9:       # ユーザーログイン後にユーザー情報のページにリダイレクトする
   10:       log_in user
(byebug) session[:forwarding_url]
"http://localhost:3000/users/1/edit"
(byebug) request.get?
true

10.3.1 演習 すべてのユーザーを表示する

10.3.1.1

<問題>レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。

<回答>

# test/integration/site_layout_test.rb
require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:michael)
  end

  test "layout links" do
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    assert_select "a[href=?]", "http://news.railstutorial.org/"
    # ログインする
    log_in_as(@user)
    get root_path
    assert_select "a[href=?]", root_path
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", edit_user_path(@user)
    assert_select "a[href=?]", logout_path
  end
end

10.3.2 演習 サンプルのユーザー

10.3.2.1

<問題>試しに他人の編集ページにアクセスしてみて、10.2.2で実装したようにリダイレクトされるかどうかを確かめてみましょう。

<回答>

/users/XXXX/editにアクセスすると、トップページにリダイレクトされる(XXXXはユーザーID)

10.3.3 演習 ページネーション

10.3.3.1

<問題>Railsコンソールを開き、pageオプションにnilをセットして実行すると、1ページ目のユーザーが取得できることを確認してみましょう。

<回答>

> User.paginate(page: nil)
  User Load (4.4ms)  SELECT  "users".* FROM "users" LIMIT ? OFFSET ?  [["LIMIT", 30], ["OFFSET", 0]]
=> [#<User:0x007fd36897ceb0
  id: 1,
  name: "Example User",
  email: "example@railstutorial.org",
  created_at: Wed, 23 Aug 2017 00:25:16 UTC +00:00,
  updated_at: Wed, 23 Aug 2017 00:25:16 UTC +00:00,
  password_digest: "$2a$10$mjXbcC/DgUxXF.uhaGsZDeueTiLUbdsXsQVso79iYlmi1Nq7hMu0q",
  remember_digest: nil,
  admin: true>,
 #<User:0x007fd36894fcf8

10.3.3.2

<問題>先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか? また、User.allのクラスとどこが違うでしょうか? 比較してみてください。

<回答>

> paginate_user = User.paginate(page: nil)
> paginate_user.class
=> User::ActiveRecord_Relation
> all_user = User.all
> all_user.class
=> User::ActiveRecord_Relation

クラスは一緒だよ!

10.3.4 演習 ユーザー一覧のテスト

10.3.4.1

<問題>試しにリスト 10.45にあるページネーションのリンク (will_paginateの部分) を2つともコメントアウトしてみて、リスト 10.48のテストが redに変わるかどうか確かめてみましょう。

<回答>

 FAIL["test_index_as_admin_including_pagination_and_delete_links", UsersIndexTest, 4.353926331999901]
 test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest (4.35s)
        Expected at least 1 element matching "div.pagination", found 0..
        Expected 0 to be >= 1.
        test/integration/users_index_test.rb:14:in `block in <class:UsersIndexTest>'

divタグのpaginationクラスが無いからエラーになる

10.3.4.2

<問題>先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが greenのままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。

<回答>

  • 1つだけコメントアウトした場合、テストが greenのままであることを確認
  • 2つとも存在していることをテストする
# test/integration/users_index_test.rb
    assert_select 'div.pagination', count: 2 # count: 2を追加

10.3.5 演習 パーシャルのリファクタリング

10.3.5.1

<問題>リスト 10.52にあるrenderの行をコメントアウトし、テストの結果が redに変わることを確認してみましょう。

<回答>

 FAIL["test_index_as_admin_including_pagination_and_delete_links", UsersIndexTest, 1.8776025120005215]
 test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest (1.88s)
        Expected at least 1 element matching "a[href="/users/14035331"]", found 0..
        Expected 0 to be >= 1.
        test/integration/users_index_test.rb:17:in `block (2 levels) in <class:UsersIndexTest>'
        test/integration/users_index_test.rb:16:in `block in <class:UsersIndexTest>'

一覧に表示されるはずのユーザーへのリンクパスが見つから無いのでエラーになります。

10.4.1 演習 管理ユーザー

10.4.1.1

<問題>Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL (/users/:id) に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は redになるはずです。

<回答>

# test/controllers/users_controller_test.rb
  test "should not allow the admin attribute to be edited via the web" do
    log_in_as(@other_user)
    assert_not @other_user.admin?
    patch user_path(@other_user), params: {
      user: {
        password: @other_user.password,
        password_confirmation: @other_user.password_confirmation,
        admin: true
      }
    }
    assert_not @other_user.reload.admin?
  end

10.4.2 演習 destroyアクション

10.4.2.1

<問題>管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。ユーザーを削除すると、Railsサーバーのログにはどのような情報が表示されるでしょうか?

<回答>

10.4.3 演習 ユーザー削除のテスト

10.4.3.1

<問題>試しにリスト 10.59にある管理者ユーザーのbeforeフィルターをコメントアウトしてみて、テストの結果が redに変わることを確認してみましょう。

<回答>

 FAIL["test_should_redirect_destroy_when_logged_in_as_a_non-admin", UsersControllerTest, 4.160433043001831]
 test_should_redirect_destroy_when_logged_in_as_a_non-admin#UsersControllerTest (4.16s)
        "User.count" didn't change by 0.
        Expected: 34
          Actual: 33
        test/controllers/users_controller_test.rb:56:in `block in <class:UsersControllerTest>'

画面からだとdeleteリンクがでないから、多分これありえないけど攻撃される時はきっとテストみたいな感じで直接やられるのだろう。 それをテストでしっかりブロックできてることを確認できるのは良いね。 で、patchとかdeleteメソッドってどうやってやるんだろうか?

railsチュートリアル10章でつまずいた

$ rails test
Running via Spring preloader in process 13479
Started with run options --seed 32097

ERROR["test_index_as_non-admin", UsersIndexTest, 1.2183937259997037]
 test_index_as_non-admin#UsersIndexTest (1.22s)
NoMethodError:         NoMethodError: undefined method `paginate' for #<Class:0x007fc03bee53b0>
            app/controllers/users_controller.rb:7:in `index'
            test/integration/users_index_test.rb:29:in `block in <class:UsersIndexTest>'

ERROR["test_index_as_admin_including_pagination_and_delete_links", UsersIndexTest, 1.2559454040001583]
 test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest (1.26s)
NoMethodError:         NoMethodError: undefined method `paginate' for #<Class:0x007fc03bee53b0>
            app/controllers/users_controller.rb:7:in `index'
            test/integration/users_index_test.rb:12:in `block in <class:UsersIndexTest>'

  37/37: [==========================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.95285s
37 tests, 93 assertions, 0 failures, 2 errors, 0 skips

rh0257:railstutorial_10 taku$ spring stop # これやったら治った!
Spring stopped.
rh0257:railstutorial_10 taku$ rails test
Running via Spring preloader in process 13604
Started with run options --seed 8584

  37/37: [==========================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.85845s
37 tests, 156 assertions, 0 failures, 0 errors, 0 skips

くそー動かしてみて問題無いのなら、一先ずほっとくのもありだね。 忘れた頃に治ってるかも。

Ruby on Rails チュートリアルで30歳までに人生を変える(第9章)

こんにちは。opiyoです。

今回は、第9章をやっていきます。

第9章は画面からユーザーを登録する方法です。

railsチュートリアル9章で学んだこと

  • 記憶トークン (remember token) を生成し、cookiesメソッドによる永続的cookiesの作成
  • 安全性の高い記憶ダイジェスト (remember digest) によるトークン認証にこの記憶トークンを活用

railsチュートリアル9章の演習解説

9.1.1 演習 記憶トークンと暗号化

9.1.1.1

<問題>コンソールを開き、データベースにある最初のユーザーを変数userに代入してください。その後、そのuserオブジェクトからrememberメソッドがうまく動くかどうか確認してみましょう。また、remember_tokenとremember_digestの違いも確認してみてください。

<回答>

* user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-03 07:19:07", updated_at: "2017-07-03 07:40:01", password_digest: "$2a$10$pQp3dmB7GUGAw/0gkC8KKuH7iS2R5afnYNlTmqg/mT5...", remember_digest: nil>
irb(main):012:0> user.remember
   (0.1ms)  begin transaction
  SQL (0.3ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", 2017-08-18 01:28:49 UTC], ["remember_digest", "$2a$10$G7Gzgfj9Ypt7BnwJVDAhfuzbxhxMXv4f67/04hjZbIT.O/BoNA.BO"], ["id", 1]]
   (1.5ms)  commit transaction
=> true
irb(main):013:0> user
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-03 07:19:07", updated_at: "2017-08-18 01:28:49", password_digest: "$2a$10$pQp3dmB7GUGAw/0gkC8KKuH7iS2R5afnYNlTmqg/mT5...", remember_digest: "$2a$10$G7Gzgfj9Ypt7BnwJVDAhfuzbxhxMXv4f67/04hjZbIT...">
irb(main):014:0> user.remember_token
=> "r_pj-i3RnMV0jGD7U9VdEg"
irb(main):015:0> user.remember_digest
=> "$2a$10$G7Gzgfj9Ypt7BnwJVDAhfuzbxhxMXv4f67/04hjZbIT.O/BoNA.BO"

9.1.1.2

<問題>リスト 9.3では、明示的にUserクラスを呼び出すことで、新しいトークンやダイジェスト用のクラスメソッドを定義しました。実際、User.new_tokenやUser.digestを使って呼び出せるようになったので、おそらく最も明確なクラスメソッドの定義方法であると言えるでしょう。しかし実は、より「Ruby的に正しい」クラスメソッドの定義方法が2通りあります。1つはややわかりにくく、もう1つは非常に混乱するでしょう。テストスイートを実行して、リスト 9.4 (ややわかりにくい) や、リスト 9.5 (非常に混乱する) の実装でも、正しく動くことを確認してみてください。ヒント: selfは、通常の文脈ではUser「モデル」、つまりユーザーオブジェクトのインスタンスを指しますが、リスト 9.4やリスト 9.5の文脈では、selfはUser「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります。

<回答>

  # 渡された文字列のハッシュ値を返す
  def self.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
   
  # ランダムなトークンを返す
  def self.new_token
    SecureRandom.urlsafe_base64
  end

問題なし

  class << self
    # 渡された文字列のハッシュ値を返す
    def digest(string)
      cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                    BCrypt::Engine.cost
      BCrypt::Password.create(string, cost: cost)
    end
      
    # ランダムなトークンを返す
    def new_token
      SecureRandom.urlsafe_base64
    end
  end

問題なし

9.1.2 演習 ログイン状態の保持

9.1.2.1

<問題>ブラウザのcookieを調べ、ログイン後のブラウザではremember_tokenと暗号化されたuser_idがあることを確認してみましょう。

<回答>

Chromeディベロッパーツールを使って確認。

remember_token:_T_91tSHmmZtIf6GLa3Pzg
user_id:5d0aa7d03c456ef4864c32703d61611e49364eaa

9.1.2.2

<問題>コンソールを開き、リスト 9.6のauthenticated?メソッドがうまく動くかどうか確かめてみましょう。

<回答>

* u = User.last
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "haku", email: "haku@co.jp", created_at: "2017-08-18 01:58:33", updated_at: "2017-08-18 02:02:05", password_digest: "$2a$10$e9Rq/VoCQN8j6.7G0CYYG.dOlVAMhhdGOidroqcUgtc...", remember_digest: "$2a$10$HXE4Ts.phrwUZUZXMJJh4ud2MsVMpEN32GT7fK/ZvjD...">
irb(main):010:0> token = "_T_91tSHmmZtIf6GLa3Pzg"
=> "_T_91tSHmmZtIf6GLa3Pzg"
irb(main):011:0> u.authenticated?(token)
=> true

9.1.3 演習 ユーザーを忘れる

9.1.3.1

<問題>ログアウトした後に、ブラウザの対応するcookiesが削除されていることを確認してみましょう。

<回答> Chromeディベロッパーツールを使ってcookiesが削除されていることを確認。

9.1.4 演習 2つの目立たないバグ

9.1.4.1

<問題>リスト 9.16で修正した行をコメントアウトし、2つのログイン済みのタブによるバグを実際に確かめてみましょう。まず片方のタブでログアウトし、その後、もう1つのタブで再度ログアウトを試してみてください。

<回答>

エラーになることを確認!

NoMethodError in SessionsController#destroy
undefined method `forget' for nil:NilClass

9.1.4.2

<問題>リスト 9.19で修正した行をコメントアウトし、2つのログイン済みのブラウザによるバグを実際に確かめてみましょう。まず片方のブラウザでログアウトし、もう一方のブラウザを再起動してサンプルアプリケーションにアクセスしてみてください。

<回答> もう一方のブラウザを再起動してサンプルアプリケーションにアクセスできちゃうことを確認!

9.1.4.3

<問題>上のコードでコメントアウトした部分を元に戻し、テストスイートが red から greenになることを確認しましょう。

<回答> テストが通ることを確認!

9.2 演習 [Remember me] チェックボックス

9.2.1

<問題>ブラウザでcookies情報を調べ、[remember me] をチェックしたときに意図した結果になっているかどうかを確認してみましょう。

<回答>

ログインが保持されていることを確認しました。 - [remember me] へのチェックあり:「remember_token」、「user_id」が保存される - [remember me] へのチェックなし:「remember_token」、「user_id」が保存されない

9.2.2

<問題>コンソールを開き、三項演算子を使った実例を考えてみてください (コラム 9.2)。

<回答>

> result = 90
=> 90
irb(main):018:0> result > 80 ? "合格":"不合格"
=> "合格"
irb(main):019:0> result = 60
=> 60
irb(main):020:0> result > 80 ? "合格":"不合格"
=> "不合格"

※僕の環境(Ruby 2.3.3)だと、日本語入力すると化ける。もし同じ現象になる場合はRubyのバージョンを変更して見てください。

$ ruby -v
ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin15]
rh0257:railstutorial_9 taku$ rbenv versions
  system
  2.1.5
  2.1.6
* 2.3.3 (set by /Users/taku/rails/railstutorial/railstutorial_9/.ruby-version)
  2.3.4
  2.4.1
rh0257:railstutorial_9 taku$ rbenv local 2.3.4
rh0257:railstutorial_9 taku$ ruby -v
ruby 2.3.4p301 (2017-03-30 revision 58214) [x86_64-darwin15]

# インストールする場合
$ rbenv install --list
$ rbenv install 2.3.4

9.3.1 演習 [Remember me] ボックスをテストする

9.3.1.1

<問題>リスト 9.25の統合テストでは、仮想のremember_token属性にアクセスできないと説明しましたが、実は、assignsという特殊なテストメソッドを使うとアクセスできるようになります。コントローラで定義したインスタンス変数にテストの内部からアクセスするには、テスト内部でassignsメソッドを使います。このメソッドにはインスタンス変数に対応するシンボルを渡します。例えばcreateアクションで@userというインスタンス変数が定義されていれば、テスト内部ではassigns(:user)と書くことでインスタンス変数にアクセスできます。本チュートリアルのアプリケーションの場合、Sessionsコントローラのcreateアクションでは、userを (インスタンス変数ではない) 通常のローカル変数として定義しましたが、これをインスタンス変数に変えてしまえば、cookiesにユーザーの記憶トークンが正しく含まれているかどうかをテストできるようになります。このアイデアに従ってリスト 9.27とリスト 9.28の不足分を埋め (ヒントとして?やFILL_INを目印に置いてあります)、[remember me] チェックボックスのテストを改良してみてください。

<回答>

class SessionsController < ApplicationController
  def new
  end

  def create
    @user = User.find_by(email: params[:session][:email].downcase) # ローカル変数`user`に@を付けてインスタンス変数へ
    if @user && @user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
      log_in @user
      params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
      redirect_to @user
    else
      # エラーメッセージを作成する
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

(略)

  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_equal cookies['remember_token'], assigns(:user).remember_token # インスタンス変数に設定された`remember_token`で比較する
  end

end

9.3.2 演習 [Remember me] をテストする

9.3.2.1

<問題>リスト 9.33にあるauthenticated?の式を削除すると、リスト 9.31の2つ目のテストで失敗することを確かめてみましょう (このテストが正しい対象をテストしていることを確認してみましょう)。

<回答>

module SessionsHelper

(略)

  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
#      if user && user.authenticated?(cookies[:remember_token]) 
      if user
        log_in user
        @current_user = user
      end
    end
  end

この状態でテストすると失敗することを確認。

Ruby on Rails チュートリアルで30歳までに人生を変える(第8章)

こんにちは。opiyoです。

今回は、第8章をやっていきます。

第8章は画面からユーザーを登録する方法です。

railsチュートリアル8章の学んだこと

  • httpはステートレスなプロトコル。つまり状態がない
  • リクエストのひとつひとつは、その前のリクエスト情報を知らない
  • リクエストが終わると何もかも忘れて次回最初からやり直す健忘症的なプロトコル
  • ブラウザのあるページから別のページに移動したときに、ユーザーのIDを保持しておく手段がHTTPプロトコル内「には」まったくあり
  • セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザRailsサーバーなど) に別途設定
  • セッションを実装する方法として最も一般的なのは、cookies
  • sessionメソッドで作成した一時cookiesは自動的に暗号化され
  • user.authenticate(password)は、引数に渡された文字列 (パスワード) をハッシュ化した値と、データベース内にあるpassword_digestカラムの値を比較する
  • 「!!」(「バンバン (bang bang)」と読みます) という演算子を使うと、そのオブジェクトを2回否定することになるので、どんなオブジェクトも強制的に論理値に変換できる
> 0
=> 0
irb(main):017:0> !0
=> false
irb(main):019:0* !!0
=> true

> user && user.authenticate("hakuhaku")
=> #<User id: 1, name: "haku", email: "haku@co.jp", created_at: "2017-08-17 02:41:54", updated_at: "2017-08-17 02:41:54", password_digest: "$2a$10$zcrCZj1hnh3cp.zZDmYD6OV/73uzwUzbWTqyceJZWEz...">
irb(main):023:0> !!(user && user.authenticate("hakuhaku"))
=> true
  • flashのメッセージとは異なり、flash.nowのメッセージはその後リクエストが発生したときに消滅します
  • sessionメソッドで作成した一時cookiesは自動的に暗号化される
  • Railsのapplication.jsファイルを通して、Bootstrapに同梱されているJavaScriptライブラリを読み込むよう、アセットパイプラインに指示する
  • Rails 5.1 からjQueryもデフォルトで読み込まれなくなったので、jQueryも追加する必要があります

railsチュートリアル8章の演習解説

8.1.1 演習 Sessionsコントローラ

8.1.1.1

<問題>GET login_pathとPOST login_pathとの違いを説明できますか? 少し考えてみましょう。

<回答>

       login GET    /login(.:format)          sessions#new
             POST   /login(.:format)          sessions#create
  • GET login_path:'/login'にアクセスした時 → sessionコントローラー、newアクション
  • POST login_path:'/login'で、入力した値を送信した時 → sessionコントローラー、createアクション

8.1.1.2

<問題>ターミナルのパイプ機能を使ってrails routesの実行結果とgrepコマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。現在、いくつのSessionsリソースがあるでしょうか? ヒント: パイプやgrepの使い方が分からない場合は Learn Enough Command Line to Be Dangerousの Section on Grep (英語) を参考にしてみてください。

<回答>

$ rails routes | grep users
      signup GET    /signup(.:format)         users#new
       users GET    /users(.:format)          users#index
             POST   /users(.:format)          users#create
    new_user GET    /users/new(.:format)      users#new
   edit_user GET    /users/:id/edit(.:format) users#edit
        user GET    /users/:id(.:format)      users#show
             PATCH  /users/:id(.:format)      users#update
             PUT    /users/:id(.:format)      users#update
             DELETE /users/:id(.:format)      users#destroy
$ rails routes | grep sessions
sessions_new GET    /sessions/new(.:format)   sessions#new
       login GET    /login(.:format)          sessions#new
             POST   /login(.:format)          sessions#create
      logout DELETE /logout(.:format)         sessions#destroy

8.1.2 演習 ログインフォーム

8.1.2.1

<問題>リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか? 考えてみてください。ヒント:表 8.1とリスト 8.5の1行目に注目してください。

<回答>

       login GET    /login(.:format)          sessions#new
             POST   /login(.:format)          sessions#create

form_for(:session, url: login_path)のurlに指定されたプレフィックスを見て判断している。 httpメソッドが、GET or POSTかはmethodオプションで変えれるはずだけどデフォルトはpostになる。 よってSessionsコントローラのcreateアクションに到達する!

8.1.3 演習 ユーザーの検索と認証

8.1.3.1

<問題>Railsコンソールを使って、表 8.2のそれぞれの式が合っているか確かめてみましょう. まずはuser = nilの場合を、次にuser = User.firstとした場合を確かめてみてください。ヒント: 必ず論理値オブジェクトとなるように、4.2.3で紹介した!!のテクニックを使ってみましょう。例: !!(user && user.authenticate(’foobar’))

<回答>

■User:存在しない && Password:合致しない → false

> user = nil
=> nil
irb(main):002:0> user && user.authenticate("hoge")
=> nil

■User:存在する && Password:合致しない → false

> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "haku", email: "haku@co.jp", created_at: "2017-08-17 02:41:54", updated_at: "2017-08-17 02:41:54", password_digest: "$2a$10$zcrCZj1hnh3cp.zZDmYD6OV/73uzwUzbWTqyceJZWEz...">
> !!(user && user.authenticate("hoge"))
=> false

■User:存在する && Password:合致する → true

> !!(user && user.authenticate("hakuhaku"))
=> true

8.1.5 演習 フラッシュのテスト

8.1.5.1

<問題>8.1.4の処理の流れが正しく動いているかどうか、ブラウザで確認してみてください。特に、flashがうまく機能しているかどうか、フラッシュメッセージの表示後に違うページに移動することを忘れないでください。

<回答>

  1. "/login"を開く
  2. EmailとPasswordに適当な値を入力しログインボタンを押下する
  3. flashメッセージが表示されることを確認
  4. "/"を開いた時にflashメッセージが表示されないことを確認

8.2.1 log_inメソッド

8.2.1.1

<問題>有効なユーザーで実際にログインし、ブラウザからcookiesの情報を調べてみてください。このとき、sessionの値はどうなっているでしょうか? ヒント: ブラウザでcookiesを調べる方法が分からない? 今こそググってみるときです! (コラム 1.1)

<回答> Chromeの場合はディベロッパーツールのApplicationタブから確認できます。(右クリックして「検証」すれば出てくる!) f:id:opiyotan:20170817122354p:plain

8.2.1.2

<問題>先ほどの演習課題と同様に、Expiresの値について調べてみてください。

<回答> 有効期限の意味。 上の画像で「Session」ってなってるやつは、ブラウザの再起動で無くなる。 年月日があるものは、それが有効期限になる。

8.2.2 演習 現在のユーザー

8.2.2.1

<問題>Railsコンソールを使って、User.find_by(id: ...)で対応するユーザーが検索に引っかからなかったとき、nilを返すことを確認してみましょう。

<回答>

> User.find_by(id: 99)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 99], ["LIMIT", 1]]
=> nil

<問題>先ほどと同様に、今度は:user_idキーを持つsessionハッシュを作成してみましょう。リスト 8.17に記したステップにしたがって、||=演算子がうまく動くことも確認してみましょう。

<回答>

> session = {}
=> {}
irb(main):002:0> session[:user_id] = nil
=> nil
irb(main):003:0> @current_user ||= User.find_by(id: session[:user_id])
  User Load (2.9ms)  SELECT  "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ?  [["LIMIT", 1]]
=> nil
irb(main):004:0> session[:user_id]= User.first.id
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> 1
irb(main):005:0> @current_user ||= User.find_by(id: session[:user_id])
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "haku", email: "haku@co.jp", created_at: "2017-08-17 02:41:54", updated_at: "2017-08-17 02:41:54", password_digest: "$2a$10$zcrCZj1hnh3cp.zZDmYD6OV/73uzwUzbWTqyceJZWEz...">

8.2.3 演習 レイアウトリンクを変更する

8.2.3.1

<問題>ブラウザのcookieインスペクタ機能を使って (8.2.1.1)、セッション用のcookieを削除してみてください。ヘッダー部分にあるリンクは非ログイン状態のものになっているでしょうか? 確認してみましょう。

<回答> 非ログイン状態になる。

8.2.3.2

<問題>もう一度ログインしてみて、ヘッダーのレイアウトが変わったことを確認してみましょう。その後、ブラウザを再起動させ、再び非ログイン状態に戻ったことも確認してみてください。注意: もしブラウザの [閉じたときの状態に戻す] 機能をオンにしていると、セッション情報も復元される可能性があります。もしその機能をオンにしている場合、忘れずにオフにしておきましょう (コラム 1.1)。

<回答> 非ログイン状態になる。

8.2.4 演習 レイアウトの変更をテストする

8.2.4.1

<問題>試しにSessionヘルパーのlogged_in?メソッドから!を削除してみて、リスト 8.23が redになることを確認してみましょう。

<回答>

$ rails test
FAIL["test_login_with_valid_information_followed_by_logout", UsersLoginTest, 0.940030867999667]
 test_login_with_valid_information_followed_by_logout#UsersLoginTest (0.94s)
        Expected exactly 0 elements matching "a[href="/login"]", found 1..
        Expected: 0
          Actual: 1
        test/integration/users_login_test.rb:27:in `block in <class:UsersLoginTest>'

8.2.4.2

<問題>先ほど削除した部分 (!) を元に戻して、テストが greenに戻ることを確認してみましょう。

<回答>

$ rails test
Running via Spring preloader in process 4789
Started with run options --seed 35483

  21/21: [==========================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.37352s
21 tests, 54 assertions, 0 failures, 0 errors, 0 skips

8.2.5 演習 レイアウトの変更をテストする

8.2.5.1

<問題>リスト 8.25のlog_inの行をコメントアウトすると、テストスイートは red になるでしょうか? それとも green になるでしょうか? 確認してみましょう。

<回答> テストスイートは redになる。

 FAIL["test_valid_signup_information", UsersSignupTest, 2.6715680599991174]
 test_valid_signup_information#UsersSignupTest (2.67s)
        Expected false to be truthy.
        test/integration/users_signup_test.rb:15:in `block in <class:UsersSignupTest>'

8.2.5.2

<問題>現在使っているテキストエディタの機能を使って、リスト 8.25をまとめてコメントアウトできないか調べてみましょう。また、コメントアウトの前後でテストスイートを実行し、コメントアウトすると red に、コメントアウトを元に戻すと green になることを確認してみましょう。ヒント: コメントアウト後にファイルを保存することを忘れないようにしましょう。また、テキストエディタコメントアウト機能については Test Editor Tutorial の Commenting Out (英語) などを参照してみてください。

<回答> コメントアウトは、コメントアウトしたい行を全て選択して「Command + /」(エディタ:Atom

8.3 演習 ログアウト

8.3.1

<問題>ブラウザから [Log out] リンクをクリックし、どんな変化が起こるか確認してみましょう。また、リスト 8.31で定義した3つのステップを実行してみて、うまく動いているかどうか確認してみましょう。

<回答> - トップページが表示される - dropdownメニューがLog inになる - ProfileSettingsのメニューが非表示になっている

8.3.2

<問題>cookiesの内容を調べてみて、ログアウト後にはsessionが正常に削除されていることを確認してみましょう。

<回答>

   18:   def destroy
   19:     log_out if logged_in?
   20:     byebug
=> 21:     redirect_to root_url
   22:   end

(byebug) session[:user_id]
nil

Ruby on Rails チュートリアルで30歳までに人生を変える(第7章)

こんにちは。opiyoです。

今回は、第7章をやっていきます。

第7章は画面からユーザーを登録する方法です。

railsチュートリアル7章の学んだこと

  • Railsにはテスト環境 (test)、開発環境 (development)、そして本番環境 (production) の3つの環境がデフォルトで装備されている
  $ rails console
  Loading development environment
  >> Rails.env
  => "development"
  >> Rails.env.development?
  => true
  >> Rails.env.test?
  => false

# rails db:migrateを本番環境で実行する
$ rails db:migrate RAILS_ENV=production
  • Sassのミックスイン機能を使うことで、CSSルールのグループをパッケージ化できる
  • resources :usersの1行を追加すると、ユーザーのURLを生成するための多数の名前付きルート (5.3.3) と共に、RESTfulなUsersリソースで必要となるすべてのアクションが利用できるようになる
  • Digestライブラリのhexdigestメソッドを使うと、MD5のハッシュ化が実現できま
>> email = "MHARTL@example.COM"
>> Digest::MD5::hexdigest(email.downcase)
=> "1fda4469bcbec3badf5418269ffc5968"
  • formの構造について
<%= f.label :name %>
<%= f.text_field :name %>
↓ #変換されると?
<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />
  • 空の判定
>> user.errors.empty?
=> false # 値が何かしらある。空ならtrue。
>> user.errors.any?
=> true # 値が何かしらある。空ならfalse
  • 引数に整数が与えられると、それに基づいて2番目の引数の英単語を複数形に変更したものを返す
>> helper.pluralize(2, "woman")
=> "2 women"
>> helper.pluralize(3, "erratum")
=> "3 errata"
  • redirect_toの仕組み
redirect_to @user
↓
redirect_to user_url(@user)
 # user_urlはプレフィックスなので、具体的に言うと`users#show`

railsチュートリアル7章ではまった

key must be 32 bytes

ArgumentError (key must be 32 bytes):
Application Trace | Framework Trace | Full Trace
activesupport (5.0.0.1) lib/active_support/message_encryptor.rb:72:in `key='
activesupport (5.0.0.1) lib/active_support/message_encryptor.rb:72:in `_encrypt'
activesupport (5.0.0.1) lib/active_support/message_encryptor.rb:58:in `encrypt_and_sign'
actionpack (5.0.0.1) lib/action_dispatch/middleware/cookies.rb:592:in `commit'
actionpack (5.0.0.1) lib/action_dispatch/middleware/cookies.rb:465:in `[]='
actionpack (5.0.0.1) lib/action_dispatch/middleware/session/cookie_store.rb:117:in `set_cookie'
rack (2.0.3) lib/rack/session/abstract/id.rb:363:in `commit_session'
rack (2.0.3) lib/rack/session/abstract/id.rb:234:in `context'
rack (2.0.3) lib/rack/session/abstract/id.rb:226:in `call'

解決方法は、rubyのバージョンを変えると上手くいった。 変更方法はこちらから。 今回、私の場合は2.4.1から2.3.4に変更し上手くいったがバージョン下げるってのは何だか嫌だな。

railsチュートリアル7章の演習解説

7.1.1 演習 デバッグRails環境

7.1.1.1

<問題> ブラウザから /about にアクセスし、デバッグ情報が表示されていることを確認してください。このページを表示するとき、どのコントローラとアクションが使われていたでしょうか? paramsの内容から確認してみましょう。

<回答>

--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: static_pages
  action: about
permitted: false

7.1.1.2

<問題>Railsコンソールを開き、データベースから最初のユーザー情報を取得し、変数userに格納してください。その後、puts user.attributes.to_yamlを実行すると何が表示されますか? ここで表示された結果と、yメソッドを使ったy user.attributesの実行結果を比較してみましょう。

<回答>

> user = User.first
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-03 07:19:07", updated_at: "2017-07-03 07:40:01", password_digest: "$2a$10$pQp3dmB7GUGAw/0gkC8KKuH7iS2R5afnYNlTmqg/mT5...">
> user.attributes.class
=> Hash
> user.attributes
=> {"id"=>1, "name"=>"Example User", "email"=>"example@railstutorial.org", "created_at"=>Mon, 03 Jul 2017 07:19:07 UTC +00:00, "updated_at"=>Mon, 03 Jul 2017 07:40:01 UTC +00:00, "password_digest"=>"$2a$10$pQp3dmB7GUGAw/0gkC8KKuH7iS2R5afnYNlTmqg/mT5oPa9JV.taW"}

irb(main):002:0> puts user.attributes.to_yaml # user.attributeでHashが返ってくるので、それをyml化させてる
---
id: 1
name: Example User
email: example@railstutorial.org
created_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &1 2017-07-03 07:19:07.610288000 Z
  zone: &2 !ruby/object:ActiveSupport::TimeZone
    name: Etc/UTC
  time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &3 2017-07-03 07:40:01.853612000 Z
  zone: *2
  time: *3
password_digest: "$2a$10$pQp3dmB7GUGAw/0gkC8KKuH7iS2R5afnYNlTmqg/mT5oPa9JV.taW"
=> nil

irb(main):004:0* y user.attributes # yメソッドはyml形式でオブジェクトの中身を表示させる。pメソッドだと<>が表示されてエスケープする必要があるがそれがいらないから利用するなんてことがあるらしい
---
id: 1
name: Example User
email: example@railstutorial.org
created_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &1 2017-07-03 07:19:07.610288000 Z
  zone: &2 !ruby/object:ActiveSupport::TimeZone
    name: Etc/UTC
  time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &3 2017-07-03 07:40:01.853612000 Z
  zone: *2
  time: *3
password_digest: "$2a$10$pQp3dmB7GUGAw/0gkC8KKuH7iS2R5afnYNlTmqg/mT5oPa9JV.taW"
=> nil

出力される結果は同じになる。

7.1.2 演習 Usersリソース

7.1.2.1

<問題>埋め込みRubyを使って、マジックカラム (created_atとupdated_at) の値をshowページに表示してみましょう (リスト 7.4)。

<回答>

<%= @user.created_at %>
<%= @user.updated_at %>

7.1.2.2

<問題>埋め込みRubyを使って、Time.nowの結果をshowページに表示してみましょう。ページを更新すると、その結果はどう変わっていますか? 確認してみてください。

<回答>

<%= Time.now %>
<%= "現在の時刻は#{Time.now.hour}#{Time.now.min}#{Time.now.sec}秒です。" %> # 時分秒だけを表示することもできる

f:id:opiyotan:20170816103859p:plain

7.1.3 演習 debuggerメソッド

7.1.3.1

<問題>showアクションの中にdebuggerを差し込み (リスト 7.6)、ブラウザから /users/1 にアクセスしてみましょう。その後コンソールに移り、putsメソッドを使ってparamsハッシュの中身をYAML形式で表示してみましょう。ヒント: 7.1.1.1の演習を参考にしてください。その演習ではdebugメソッドで表示したデバッグ情報を、どのようにしてYAML形式で表示していたでしょうか? <回答>

(byebug) puts params.to_yaml
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: show
  id: '2'
permitted: false

7.1.3.2

<問題>newアクションの中にdebuggerを差し込み、/users/new にアクセスしてみましょう。@userの内容はどのようになっているでしょうか? 確認してみてください。

<回答>

(byebug) @user
#<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil>

7.1.4 演習 Gravatar画像とサイドバー

7.1.4.1

<問題>(任意) Gravatar上にアカウントを作成し、あなたのメールアドレスと適当な画像を紐付けてみてください。メールアドレスをMD5ハッシュ化して、紐付けた画像がちゃんと表示されるかどうか試してみましょう。

<回答>

f:id:opiyotan:20170816110346p:plain

7.1.4.2

<問題>7.1.4で定義したgravatar_forヘルパーをリスト 7.12のように変更して、sizeをオプション引数として受け取れるようにしてみましょう。うまく変更できると、gravatar_for user, size: 50といった呼び出し方ができるようになります。重要: この改善したヘルパーは10.3.1で実際に使います。忘れずに実装しておきましょう。

<回答>

module UsersHelper

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user, options = { size: 80 })
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    size = options[:size]
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

7.1.4.3

<問題>オプション引数は今でもRubyコミュニティで一般的に使われていますが、Ruby 2.0から導入された新機能「キーワード引数 (Keyword Arguments)」でも実現することができます。先ほど変更したリスト 7.12を、リスト 7.13のように置き換えてもうまく動くことを確認してみましょう。この2つの実装方法はどういった違いがあるのでしょうか? 考えてみてください。

<回答>

module UsersHelper

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user, size: 80) # ここを修正! シンプルにかける。
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

7.2.1 演習 ユーザー登録フォーム

7.2.1.1

<問題>試しに、リスト 7.15にある:nameを:nomeに置き換えてみましょう。どんなエラーメッセージが表示されるようになりますか?

<回答>

Unknown action
The action 'index' could not be found for UsersController

7.2.1.2

<問題>試しに、ブロックの変数fをすべてfoobarに置き換えてみて、結果が変わらないことを確認してみてください。確かに結果は変わりませんが、変数名をfoobarとするのはあまり良い変更ではなさそうですね。その理由について考えてみてください。

<回答> 結果は変わらないが、なぜダメなのか? - 変数名が長くなり見通しが悪い - formfだからこそ、意味が通じるのがfoobarだと違う意味があるように思えてしまう。混乱する。

7.2.2 演習 フォームHTML

7.2.2.1

<問題> ごめんなさい。分かりません。 教えてください!!

7.3.2 演習 Strong Parameters

7.3.2.1

<問題>/users/new?admin=1 にアクセスし、paramsの中にadmin属性が含まれていることをデバッグ情報から確認してみましょう。

<回答>

--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  admin: '1'
  controller: users
  action: new
permitted: false

7.3.3 演習 エラーメッセージ

7.3.3.1

<問題>最小文字数を5に変更すると、エラーメッセージも自動的に更新されることを確かめてみましょう。

<回答>

f:id:opiyotan:20170816124511p:plain

7.3.3.2

<問題>未送信のユーザー登録フォーム (図 7.12) のURLと、送信済みのユーザー登録フォーム (図 7.18) のURLを比べてみましょう。なぜURLは違っているのでしょうか? 考えてみてください。

<回答> - 未送信:/signup - 送信済:/users 正しく保存できなかった場合は、render 'new'する処理となっているため。

7.3.4 演習 失敗時のテスト

7.3.4.1

<問題>リスト 7.20で実装したエラーメッセージに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト 7.25にテンプレートを用意しておいたので、参考にしてください。

<回答>

# test/integration/users_signup_test.rb
  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_template 'users/new'
    assert_select 'div#error_explanation'
    assert_select 'div.alert-danger'
  end

7.3.4.2

<問題>未送信のユーザー登録フォームと送信直後のURLは、それぞれ /signup と /users になり、URLが異なっています。これは、リスト 5.43で追加した名前付きルートと、デフォルトのRESTfulなルーティング (リスト 7.3) を設定したことによって生じた差異です。リスト 7.26とリスト 7.27の内容を追加し、この問題を解決してみてください。うまくいけば、いずれのURLも /signup となるはずです。あれ、でもテストは greenのままになっていますね...、なぜでしょうか? (考えてみてください)

<回答>

Rails.application.routes.draw do
  root 'static_pages#home'
(略)
  get  '/signup',  to: 'users#new'
  post '/signup',  to: 'users#create' # ここを追加
  resources :users
end
# app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user, url: signup_path) do |f| %> # ここを修正・・・明示的にurlを指定する(users#create)
(略)
      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
# rails routes
   signup GET    /signup(.:format)         users#new
          POST   /signup(.:format)         users#create
    users GET    /users(.:format)          users#index
          POST   /users(.:format)          users#create

見て分かるように、変更前はuserspostアクションを利用してusers#createの処理となっていた。 変更後は、signuppostアクションをしたusers#createの処理となっている。 アクション先がどちらの書き方でも変わらないため、テストを修正しなくても成功する。

7.3.4.3

<問題>リスト 7.25のpost部分を変更して、先ほどの演習課題で作った新しいURL (/signup) に合わせてみましょう。また、テストが greenのままになっている点も確認してください。

<回答>

  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post signup_path, params: { user: { name:  "", # ここを修正
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_template 'users/new'
    assert_select 'div#error_explanation'
    assert_select 'div.alert-danger'
  end

7.3.4.4

<問題>リスト 7.27のフォームを以前の状態 (リスト 7.20) に戻してみて、テストがやはり greenになっていることを確認してください。これは問題です! なぜなら、現在postが送信されているURLは正しくないのですから。assert_selectを使ったテストをリスト 7.25に追加し、このバグを検知できるようにしてみましょう (テストを追加して redになれば成功です)。その後、変更後のフォーム (リスト 7.27) に戻してみて、テストが green になることを確認してみましょう。ヒント: フォームから送信してテストするのではなく、’form[action="/signup"]’という部分が存在するかどうかに着目してテストしてみましょう。

<回答>

# test/integration/users_signup_test.rb
  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post signup_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_template 'users/new'
    assert_select "form[action=?]", "/signup" # ここを追加
    assert_select 'div#error_explanation'
    assert_select 'div.alert-danger'
  end

7.4.1 演習 登録フォームの完成

7.4.1.1

<問題>有効な情報を送信し、ユーザーが実際に作成されたことを、Railsコンソールを使って確認してみましょう。

<回答>

> User.last
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 5, name: "toma", email: "toma@co.jp", created_at: "2017-08-16 04:51:43", updated_at: "2017-08-16 04:51:43", password_digest: "$2a$10$3NSYCM0YE340GHAaaEJXlOChlsFABFemcj6CiT4yd9V...">

<問題>リスト 7.28を更新し、redirect_to user_url(@user)とredirect_to @userが同じ結果になることを確認してみましょう。

<回答>

# redirect_to @userの時
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: show
  id: '5'
permitted: false
# redirect_to user_url(@user)の時
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: show
  id: '6'
permitted: false

コントローラー、アクションが変わらない事を確認しました。

7.4.2 演習 flash

7.4.2.1

<問題>コンソールに移り、文字列内の式展開 (4.2.2) でシンボルを呼び出してみましょう。例えば"#{:success}"といったコードを実行すると、どんな値が返ってきますか? 確認してみてください。

<回答>

> "#{:success}"
=> "success"

<問題>先ほどの演習で試した結果を参考に、リスト 7.30のflashはどのような結果になるか考えてみてください。

<回答>

> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", :danger=>"It failed."}
> "#{flash[:success]}"
=> "It worked!"
irb(main):012:0> "#{flash[:danger]}"
=> "It failed."

7.4.3 演習 実際のユーザー登録

7.4.3.1

<問題>Railsコンソールを使って、新しいユーザーが本当に作成されたのかもう一度チェックしてみましょう。結果は、リスト 7.32のようになるはずです。

<回答>

> User.last
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 3, name: "toma", email: "toma@co.jp", created_at: "2017-08-16 08:19:23", updated_at: "2017-08-16 08:19:23", password_digest: "$2a$10$sYvSxyb8klPNf2D6XMhxDOu1SGev2X0lH58lZVrgLij...">
irb(ma

問題なし

<問題>自分のメールアドレスでユーザー登録を試してみましょう。既にGravatarに登録している場合、適切な画像が表示されているか確認してみてください。

<回答> 確認できた!

7.4.4 演習 実際のユーザー登録

7.4.4.1

<問題>7.4.2で実装したflashに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト 7.34に最小限のテンプレートを用意しておいたので、参考にしてください (FILL_INの部分を適切なコードに置き換えると完成します)。ちなみに、テキストに対するテストは壊れやすいです。文量の少ないflashのキーであっても、それは同じです。筆者の場合、flashが空でないかをテストするだけの場合が多いです。

<回答>

  test "valid signup information" do
    get signup_path
(略)
    assert_template 'users/show'
    assert_not flash.empty?
  end

7.4.4.2

<問題>本文中でも指摘しましたが、flash用のHTML (リスト 7.31) は読みにくいです。より読みやすくしたリスト 7.35のコードに変更してみましょう。変更が終わったらテストスイートを実行し、正常に動作することを確認してください。なお、このコードでは、Railsのcontent_tagというヘルパーを使っています。

<回答>

<!DOCTYPE html>
<html>
      .
      <% flash.each do |message_type, message| %>
        <%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
      <% end %>
      .
</html>

7.4.4.3

<問題>リスト 7.28のリダイレクトの行をコメントアウトすると、テストが失敗することを確認してみましょう。

<回答>

 rails test
Running via Spring preloader in process 7332
Started with run options --seed 36041

ERROR["test_valid_signup_information", UsersSignupTest, 2.10871023800064]
 test_valid_signup_information#UsersSignupTest (2.11s)
RuntimeError:         RuntimeError: not a redirect! 204 No Content
            test/integration/users_signup_test.rb:27:in `block in <class:UsersSignupTest>'

  19/19: [==========================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.45401s
19 tests, 40 assertions, 0 failures, 1 errors, 0 skips

7.4.4.4

<問題>リスト 7.28で、@user.saveの部分をfalseに置き換えたとしましょう (バグを埋め込んでしまったと仮定してください)。このとき、assert_differenceのテストではどのようにしてこのバグを検知するでしょうか? テストコードを追って考えてみてください。

<回答>

$ rails test
Running via Spring preloader in process 7468
Started with run options --seed 62890

 FAIL["test_valid_signup_information", UsersSignupTest, 1.01124269200227]
 test_valid_signup_information#UsersSignupTest (1.01s)
        "User.count" didn't change by 1.
        Expected: 1
          Actual: 0
        test/integration/users_signup_test.rb:21:in `block in <class:UsersSignupTest>'

 FAIL["test_invalid_signup_information", UsersSignupTest, 1.0781876540022495]
 test_invalid_signup_information#UsersSignupTest (1.08s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/users_signup_test.rb:15:in `block in <class:UsersSignupTest>'

  19/19: [==========================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.28741s
19 tests, 39 assertions, 2 failures, 0 errors, 0 skips
  • 登録されたユーザーの件数が増えないことで、assert_differenceの結果がNGになる。

7.5.3 演習 本番環境へのデプロイ

7.5.3.1

<問題>ブラウザから本番環境 (Heroku) にアクセスし、SSLの鍵マークがかかっているか、URLがhttpsになっているかどうかを確認してみましょう。

<回答> f:id:opiyotan:20170816180959p:plain

<問題>本番環境でユーザーを作成してみましょう。Gravatarの画像は正しく表示されているでしょうか?

<回答>

f:id:opiyotan:20170816181057p:plain