おぴよの気まぐれ日記

おぴよの気まぐれ日記

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

プログラミング初心者でも出来た!ビットコイン自動売買システムをRubyで作る

こんにちは。opiyoです。

今日は私が入会している人生逃げ切りサロンのメンバーである迫 佑樹さんが

Rubyで作る! ビットコイン自動売買システム

の作り方を解説した動画について紹介させてもらいます。


この動画の最大の魅力はプログラミング経験が全くない人でもPCさえあれば誰でも作れてしまうところです!

私は、この動画で学んだことをフル活用して自分でも簡単なRailsアプリを作って見ました。

これについても最後に簡単に紹介できればと思います。


皆さん。プログラミングを学べば誰でも金持ちになれるそうですよ?

そのきっかけを迫 佑樹さんから学ばせてもらいましょう。

迫 佑樹(さこ ゆうき)さんって何者?

f:id:opiyotan:20170903020123p:plain

先ずは簡単ではありますが迫さんの紹介です。

  • 現役の大学生
  • Web、iPhoneアプリのフリーランスエンジニア
  • プログラミングスクールの現役講師
  • 月間12万PVのブログ「ロボット・IT雑食日記」を運営(はてなブログで良くホットエントリーしてますよね)
  • ブログの中の大人気記事「暗記しない数学」が書籍化

とんでもなくスゲー人ってことが伝わると思います。

今回紹介する動画は、現役のプログラミングスクール講師である迫さんが作っているってのが僕はポイントだと思っていて本当に分かりやすいです。

ブログの中にもプログラミング勉強法についての記事があったり、色々な言語の入門記事があったりと見ているだけでプログラミング力が上がります。

動画の内容

では、本題。

このRubyで作る! ビットコイン自動売買システムの動画で何が学べるのか。大きくは2つです。

1. Rubyの基礎

正直これだけでもめちゃくちゃ勉強になりますし、くどいですがとんでもなく分かりやすい。さすが先生です。


例えば、「配列」

話の流れとしては、各教科のテストの点数の平均を出したい。 その場合は以下のように解くことが出来ます。

japanese = 80
math = 60
science = 30
history = 60
english = 70

(japanese + math + science + history + english) / 5

だけどこれが100人分ってなると、この組み合わせが100個準備しないといけない → これは大変。じゃーどうするのか。

見ないな形で、何故この仕組み/工夫が必要なのかが順を追って説明してくれるのでイメージしやすいのです。(あー俺の説明が逆に分かりづらくしてしまっている気がしますが、是非動画を。動画を見てください。)


その他の内容としては、こんな感じです。

  • 条件分岐(if …)
  • 繰り返し文(while …)
  • 変数(hoge = 80)
  • 配列(score = [80,30,60,50,20])
  • ハッシュ(score = “japanese” => 80, “math” => 60 …)
  • メソッド(def hoge …. end)

この基礎の部分が理解できれば色々出来ることが一気に広がると思います。その基礎固めに、この動画完璧な内容だと僕は感じました。

2. bitFlyerAPIの基礎

bitFlyerってのは今流行りの仮想通貨ビットコインの取引所になります。

このビットコイン取引所とのやり取りをAPIと呼ばれる仕組みを使ってプログラミングを学んでいきます。

最後までやり切ると、自動でビットコインの購入/売却が可能になっちゃいます。


僕は全然こういう知識が無いのですが、きっとここまでのことが出来てしまうと更に応用を効かせて本当に自動で何もせずとも稼ぐ人が現るのでは無いでしょうか。

動画を見て作ったサービスの紹介

お見せする程のものではありませんが、今回動画で学んだ経験を生かして現在の各仮想通貨の値段が幾らなのかを表示するWebアプリをRuby on Railsを使って作成してみました。

実際のソースコードとサイトURLを以下になります。

このソースコードの紹介は改めてできればと思います。

最後に

数多くあるRubyの基礎勉強ページですが、これを見てしまうと全部無駄に感じてしまいます。 それくらい価値ある動画だと感じました。

何かサービス/アプリを作るのに最低限の知識を、これでもかってくらい分かりやすく解説されています。


プログラミングの本って入門本を一冊やり切るだけで大変で、当初思い描いていた「こんなの作りたい!」って気持ちを忘れてしまうこと多くあると思います。

ですが、このRubyで作る! ビットコイン自動売買システムであれば1時間くらいで基礎についてはバッチリ学ぶことが出来ますし動画なのでつまづくことが無いのも素敵なポイントです。


プログラミングが出来ると人生は変わると多くの人が言っていますし、これを実現している人もいっぱいいます。

そのきっかけをこの動画からってのは大いにある話だと思いますので、皆さんも是非楽しんでプログラミングを学び明るい未来を切り開きましょう!

Ruby on Railsチュートリアルの環境はCloud9で決まり!

こんにちは。opiyoです。

Webアプリケーションの勉強をする際に先ず引っかかるので環境構築ではないでしょうか。

  1. 参考書や記事の通りやってるのにエラーになる。
  2. ググって色々やってみる
  3. 解決できない
  4. 辞める

これ凄いもったいないですよね。せっかく何かを学びたいと思ってもその手前で諦めてしまう。

近くに分かる人が入れば良いですが、中々そんな状況もない。私も何度も経験があります。

ですがそんな悩みは昔のこと。既に出来上がった環境を使えるサービスがあるそうです。それが

Cloud9


無料だよ!

Cloud9とは

クラウド上に構築された開発環境を私たちが使えるようにしたサービスです。

Cloud9へユーザー登録する

先ずはユーザー登録していきます。この際に一点だけ注意点はクレジットカードが必要になります。 普通に使う分には料金は発生しないので、ご安心ください。

  1. 「メールアドレス」を登録してNEXTボタンをクリック
  2. 「名前」を入力してNEXTボタンをクリック
  3. 「develloer(開発なのか趣味なのか」と「なぜ使うのか(仕事なのか趣味なのか」を洗濯してNEXTボタンをクリック
  4. 1.2.3の内容を確認してNEXTボタンをクリック
  5. クレジットカードの情報を入力してNEXTボタンをクリック(無料なので安心を)
  6. ロボットじゃなければCreate accountボタンをクリック

ユーザー登録が完了するとこんな感じの画面が表示されるよ。 f:id:opiyotan:20170901175711p:plain

Cloud9でプロジェクトを作成する

では実際にプロジェクトを作っていこうと思います。

今回は、Railsチュートリアルをやるための設定を行っていきます。

  1. 「Create a new workspace」をクリック
  2. 「Workspace name:rails-tutorial」「Description:Railsチュートリアル」「Choose a template:Railsチュートリアルのアイコン」を入力、選択します。 f:id:opiyotan:20170901180246p:plain

そうするとCloud9の環境が立ち上がります! f:id:opiyotan:20170901180503p:plain

すげー多分これで、もー出来た。ここまできっと10分ですよ!

奥さんどうですか?

Cloud9の使い方

Railsプロジェクトを作成するところまでを進めて行こうと思います。

先ず最初に使う所が一番見慣れない下側にあるコマンドラインです。

Windowsだとコマンドプロンプトに当たるのかな。Macだとターミナルですね。

これらは普段全く使わない部分だと思うのですが、Railsプロジェクトを作っていく際は多くのコマンドを使ってプロジェクトを作っていきます。

ではRailsプロジェクトを作ってみましょう。

以下の通りにコマンドを入力し実際にRailsアプリケーションを作ってみましょう!

$ gem install rails -v 5.0.3
$ rails _5.0.3_ new hello_app
$ cd hello_app/
$ bundle install
$ rails s -b $IP -p $PORT 

f:id:opiyotan:20170901183736p:plain

f:id:opiyotan:20170901183802p:plain


ここまで表示されれば一先ずOKですかね。

今後、実際にRailsチュートリアルを進めていく場合は、こちらの記事を参考にしてください。 opiyotan.hatenablog.com


では、本日はここまで。 Cloud9を使うとあっという間に開発環境が整います。Macがあればローカルに環境作るのも良いですが、上手くいかずやらなくなってしまうぐらいならCloud9を使ってみるのも良いと思います。

【完全版】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

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