プログラミング初心者でも出来た!ビットコイン自動売買システムをRubyで作る
こんにちは。opiyoです。
今日は私が入会している人生逃げ切りサロンのメンバーである迫 佑樹さんが
の作り方を解説した動画について紹介させてもらいます。
この動画の最大の魅力はプログラミング経験が全くない人でもPCさえあれば誰でも作れてしまうところです!
私は、この動画で学んだことをフル活用して自分でも簡単なRailsアプリを作って見ました。
これについても最後に簡単に紹介できればと思います。
皆さん。プログラミングを学べば誰でも金持ちになれるそうですよ?
そのきっかけを迫 佑樹さんから学ばせてもらいましょう。
迫 佑樹(さこ ゆうき)さんって何者?
先ずは簡単ではありますが迫さんの紹介です。
- 現役の大学生
- Web、iPhoneアプリのフリーランスエンジニア
- プログラミングスクールの現役講師
- 月間12万PVのブログ「ロボット・IT雑食日記」を運営(はてなブログで良くホットエントリーしてますよね)
- ブログの中の大人気記事「暗記しない数学」が書籍化
とんでもなくスゲー人ってことが伝わると思います。
今回紹介する動画は、現役のプログラミングスクール講師である迫さんが作っているってのが僕はポイントだと思っていて本当に分かりやすいです。
ブログの中にもプログラミング勉強法についての記事があったり、色々な言語の入門記事があったりと見ているだけでプログラミング力が上がります。
動画の内容
では、本題。
このRubyで作る! ビットコイン自動売買システムの動画で何が学べるのか。大きくは2つです。
1. Rubyの基礎
正直これだけでもめちゃくちゃ勉強になりますし、くどいですがとんでもなく分かりやすい。さすが先生です。
例えば、「配列」
話の流れとしては、各教科のテストの点数の平均を出したい。 その場合は以下のように解くことが出来ます。
japanese = 80 math = 60 science = 30 history = 60 english = 70 (japanese + math + science + history + english) / 5
だけどこれが100人分ってなると、この組み合わせが100個準備しないといけない → これは大変。じゃーどうするのか。
見ないな形で、何故この仕組み/工夫が必要なのかが順を追って説明してくれるのでイメージしやすいのです。(あー俺の説明が逆に分かりづらくしてしまっている気がしますが、是非動画を。動画を見てください。)
その他の内容としては、こんな感じです。
- 条件分岐(if …)
- 繰り返し文(while …)
- 変数(hoge = 80)
- 配列(score = [80,30,60,50,20])
- ハッシュ(score = “japanese” => 80, “math” => 60 …)
- メソッド(def hoge …. end)
この基礎の部分が理解できれば色々出来ることが一気に広がると思います。その基礎固めに、この動画完璧な内容だと僕は感じました。
2. bitFlyerAPIの基礎
bitFlyerってのは今流行りの仮想通貨ビットコインの取引所になります。
このビットコイン取引所とのやり取りをAPIと呼ばれる仕組みを使ってプログラミングを学んでいきます。
最後までやり切ると、自動でビットコインの購入/売却が可能になっちゃいます。
僕は全然こういう知識が無いのですが、きっとここまでのことが出来てしまうと更に応用を効かせて本当に自動で何もせずとも稼ぐ人が現るのでは無いでしょうか。
動画を見て作ったサービスの紹介
お見せする程のものではありませんが、今回動画で学んだ経験を生かして現在の各仮想通貨の値段が幾らなのかを表示するWebアプリをRuby on Railsを使って作成してみました。
実際のソースコードとサイトURLを以下になります。
- ソースコード:https://github.com/nakanoTaku/bitcoin-price-check
- 実際のサイト:https://still-inlet-93643.herokuapp.com/
このソースコードの紹介は改めてできればと思います。
最後に
数多くあるRubyの基礎勉強ページですが、これを見てしまうと全部無駄に感じてしまいます。 それくらい価値ある動画だと感じました。
何かサービス/アプリを作るのに最低限の知識を、これでもかってくらい分かりやすく解説されています。
プログラミングの本って入門本を一冊やり切るだけで大変で、当初思い描いていた「こんなの作りたい!」って気持ちを忘れてしまうこと多くあると思います。
ですが、このRubyで作る! ビットコイン自動売買システムであれば1時間くらいで基礎についてはバッチリ学ぶことが出来ますし動画なのでつまづくことが無いのも素敵なポイントです。
プログラミングが出来ると人生は変わると多くの人が言っていますし、これを実現している人もいっぱいいます。
そのきっかけをこの動画からってのは大いにある話だと思いますので、皆さんも是非楽しんでプログラミングを学び明るい未来を切り開きましょう!
Ruby on Railsチュートリアルの環境はCloud9で決まり!
こんにちは。opiyoです。
Webアプリケーションの勉強をする際に先ず引っかかるので環境構築ではないでしょうか。
- 参考書や記事の通りやってるのにエラーになる。
- ググって色々やってみる
- 解決できない
- 辞める
これ凄いもったいないですよね。せっかく何かを学びたいと思ってもその手前で諦めてしまう。
近くに分かる人が入れば良いですが、中々そんな状況もない。私も何度も経験があります。
ですがそんな悩みは昔のこと。既に出来上がった環境を使えるサービスがあるそうです。それが
Cloud9
無料だよ!
Cloud9とは
クラウド上に構築された開発環境を私たちが使えるようにしたサービスです。
Cloud9へユーザー登録する
先ずはユーザー登録していきます。この際に一点だけ注意点はクレジットカードが必要になります。 普通に使う分には料金は発生しないので、ご安心ください。
- 「メールアドレス」を登録してNEXTボタンをクリック
- 「名前」を入力してNEXTボタンをクリック
- 「develloer(開発なのか趣味なのか」と「なぜ使うのか(仕事なのか趣味なのか」を洗濯してNEXTボタンをクリック
- 1.2.3の内容を確認してNEXTボタンをクリック
- クレジットカードの情報を入力してNEXTボタンをクリック(無料なので安心を)
- ロボットじゃなければCreate accountボタンをクリック
ユーザー登録が完了するとこんな感じの画面が表示されるよ。
Cloud9でプロジェクトを作成する
では実際にプロジェクトを作っていこうと思います。
今回は、Railsチュートリアルをやるための設定を行っていきます。
- 「Create a new workspace」をクリック
- 「Workspace name:rails-tutorial」「Description:Railsチュートリアル」「Choose a template:Railsチュートリアルのアイコン」を入力、選択します。
そうするとCloud9の環境が立ち上がります!
すげー多分これで、もー出来た。ここまできっと10分ですよ!
奥さんどうですか?
Cloud9の使い方
Railsプロジェクトを作成するところまでを進めて行こうと思います。
先ず最初に使う所が一番見慣れない下側にあるコマンドラインです。
Windowsだとコマンドプロンプトに当たるのかな。Macだとターミナルですね。
これらは普段全く使わない部分だと思うのですが、Railsプロジェクトを作っていく際は多くのコマンドを使ってプロジェクトを作っていきます。
ではRailsプロジェクトを作ってみましょう。
以下の通りにコマンドを入力し実際にRailsアプリケーションを作ってみましょう!
$ gem install rails -v 5.0.3 $ rails _5.0.3_ new hello_app $ cd hello_app/ $ bundle install $ rails s -b $IP -p $PORT
ここまで表示されれば一先ずOKですかね。
今後、実際にRailsチュートリアルを進めていく場合は、こちらの記事を参考にしてください。 opiyotan.hatenablog.com
では、本日はここまで。
Cloud9を使うとあっという間に開発環境が整います。Macがあればローカルに環境作るのも良いですが、上手くいかずやらなくなってしまうぐらいならCloud9を使ってみるのも良いと思います。
【完全版】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
この状態でテストすると失敗することを確認。