【完全版】RubyonRailsのActiveRecord基礎!
こんにちは。opiyoです。
今日はRailsの勉強をしていると出てくる「Active Record 」について、勉強したいと思います。
こんな奴らですね。
ではでは早速、行ってみましょう。
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歳の夏(演習問題の回答あり)
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.followed
とactive_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.erb
とrelationships/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.html
、format.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'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_paginate
とbootstrap-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があることを確認してみましょう。
<回答>
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がうまく機能しているかどうか、フラッシュメッセージの表示後に違うページに移動することを忘れないでください。
<回答>
- "/login"を開く
- EmailとPasswordに適当な値を入力しログインボタンを押下する
- flashメッセージが表示されることを確認
- "/"を開いた時にflashメッセージが表示されないことを確認
8.2.1 log_inメソッド
8.2.1.1
<問題>有効なユーザーで実際にログインし、ブラウザからcookiesの情報を調べてみてください。このとき、sessionの値はどうなっているでしょうか? ヒント: ブラウザでcookiesを調べる方法が分からない? 今こそググってみるときです! (コラム 1.1)
<回答> Chromeの場合はディベロッパーツールのApplicationタブから確認できます。(右クリックして「検証」すれば出てくる!)
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
になる
- Profile
、Settings
のメニューが非表示になっている
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}秒です。" %> # 時分秒だけを表示することもできる
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ハッシュ化して、紐付けた画像がちゃんと表示されるかどうか試してみましょう。
<回答>
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とするのはあまり良い変更ではなさそうですね。その理由について考えてみてください。
<回答>
結果は変わらないが、なぜダメなのか?
- 変数名が長くなり見通しが悪い
- form
のf
だからこそ、意味が通じるのが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に変更すると、エラーメッセージも自動的に更新されることを確かめてみましょう。
<回答>
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
見て分かるように、変更前はusers
のpost
アクションを利用してusers#create
の処理となっていた。
変更後は、signup
のpost
アクションをした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になっているかどうかを確認してみましょう。
<回答>
<問題>本番環境でユーザーを作成してみましょう。Gravatarの画像は正しく表示されているでしょうか?
<回答>