Railsチュートリアル 第14章
14.1 Relationshipモデル
14.1.1
マイグレーションした際にschema.rb
に反映されませんでした. しかしバッファにはなんのエラーも出ませんでした. ちょっと調べてみましょう.
以下のコマンドでデータベースに接続できます.
$ rails db
私のようにDockerで環境構築している場合は次のようになるでしょう. ちなみに私はMySQLをRailsとは別のコンテナで起動させています.
$ docker-compose exec web rails db
するとパスワードが求められるので, database.yml
で指定したパスワードを入力します. すると以下のようなプロンプトが表示されます.
MySQL [myapp_development]>
myapp_development
はデータベース名ですね. マイグレーションしたテーブルが存在するかを確認してみます.
MySQL [myapp_development]> show tables; +-----------------------------+ | Tables_in_myapp_development | +-----------------------------+ | ar_internal_metadata | | microposts | | relationships | | schema_migrations | | users | +-----------------------------+ 5 rows in set (0.01 sec)
しかしschema.rb
に反映されていないのはおかしいので, マイグレーションをやりなおしましょう.
MySQL [myapp_development]> exit Bye $ docker-compose exec web rails db:rollback $ docker-compose exec web rails db:migrate
すると以下のようなエラーが出ました.
Mysql2::Error: Table 'relationships' already exists
そもそも最初のマイグレーションがうまくいってないから, ロールバックによってテーブルが削除されなかったようですね. では手動でテーブルを削除しましょう.
$ docker-compose exec web rails db MySQL [myapp_development]> drop table relationships; Query OK, 0 rows affected (0.02 sec) MySQL [myapp_development]> exit Bye
ここでもう一度マイグレーションをしたところ, ちゃんとschema.rb
に反映されました.
演習1
答えが問題文中に書かれていますね. 図14.7を見ると以下の配列が得られることがわかります.
[2, 7, 10, 8]
演習2
id
が2であるユーザーはid
が1のユーザーしかフォローしていません. したがってこのユーザーだけからなる配列が得られます.
したがってuser.followind.map(&:id)
は[1]
に等しいですね.
14.1.2
演習1, 2
省略.
14.1.3
演習1
たしかにバリデーションを解除してもテストは成功しました.
14.1.4
演習1, 2
以下の通りです.
>> first = User.first >> second = User.second >> first.following?(second) SELECT 1 AS one FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`followed_id` WHERE `relationships`.`follower_id` = 1 AND `users`.`id` = 2 LIMIT 1 => false >> first.follow(second) INSERT INTO `relationships` (`follower_id`, `followed_id`, `created_at`, `updated_at`) VALUES (1, 2, '2018-07-05 15:19:20', '2018-07-05 15:19:20') >> first.following?(second) => true >> first.unfollow(second) DELETE FROM `relationships` WHERE `relationships`.`id` = 1 >> first.following?(second) => false
14.1.5
演習1
>> user = User.first >> second = User.second >> last = User.last >> second.follow(user) >> last.follow(user) >> user.followers.map(&:id) SELECT `users`.* FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`followed_id` = 1 => [2, 100]
演習2, 3
前者はデータベースの中で数えていますが, 後者はRubyの配列として数えています. したがって前者のほうがパフォーマンスが高いですね.
user.followers.count SELECT COUNT(*) FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`followed_id` = 1 => 2 >> user.followers.to_a.count => 2
14.2 [follow]のWebインターフェース
14.2.1
演習1, 2
>> user = User.first >> user.following.count => 49 >> user.followers.count => 38
14.2.2
演習1, 2
省略.
演習3
プロフィールテストに書きました.
# test/integration/users_profile_test.rb test "stats" do log_in_as(@user) get root_path assert_select '#following', text: "#{@user.following.count}" assert_select '#followers', text: "#{@user.followers.count}" get user_path(@user) assert_select '#following', text: "#{@user.following.count}" assert_select '#followers', text: "#{@user.followers.count}" end
以下のように数字のままだとダメでした.
assert_select '#following', text: @user.following.count
14.2.3
演習1, 2
省略.
14.2.4
演習1
省略.
演習2
Follow, Unfollowどちらを押しても, users/show
が描画されています.
Rendered users/show.html.erb within layouts/application
14.2.5
演習1
Safariの開発メニューからJavascriptを無効にした場合もちゃんと動作しました.
演習2
省略.
14.2.6
演習1, 2
省略.
14.3 ステータスフィード
14.3.1
演習1
新しい順に取得するので, 以下のようになるでしょう.
user.feed.map(&:id) => [10, 9, 7, 5, 4, 2, 1]
14.3.2
演習1
feed
メソッドを以下のように変更すればいいですね.
def feed Micropost.where("user_id IN (?)", following_ids) end
この場合はmichael
自身の投稿を確認するテストで失敗します.
演習2
これは13章で実装した通りですね.
def feed Micropost.where("user_id = ?", id) end
するとlana
の投稿を確認するテストで失敗します.
演習3
feed
メソッドを以下のように変更すればいいですね.
def feed Micropost.all end
するとarcher
の投稿を確認するテストで失敗します.
14.3.3
演習1
以下の通りです.
test "feed on Home page" do get root_path @user.feed.paginate(page: 1) do |micropost| assert_match CGI.escapeHTML(micropost.content), response.body end end
演習2
ヒントから, 以下のmicropostがまずいんでしょうが, よくわかりませんでした.
tone: content: "I'm sorry. Your words made sense, but your sarcastic tone did not." created_at: <%= 10.minutes.ago %> user: lana
Railsチュートリアル 第13章
13.1 Micropostモデル
13.1.1
演習1
インスタンスを作成しただけなので, マジックカラムの中身はnil
ですね.
>> micropost = Micropost.new(content: "Lorem ipsum", user_id: User.first.id) >> micropost.created_at => nil
演習2
関連付けがされているモデル名がアクセサになっているんですね.
>> micropost.user User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 => #<User id: 1, name: "Example User", email: "example@railstutorial.org", (省略)> >> micropost.user.name => "Example User"
演習3
データベースに保存すれば, そこでタイムスタンプが押されます.
>> micropost.save >> micropost.created_at => Wed, 04 Jul 2018 04:11:06 UTC +00:00
そういえばこの場合はリロードしなくていいんですね.
13.1.2
演習1
>> micropost = Micropost.new >> micropost.valid? => false >> micropost.errors.full_messages => ["User must exist", "User can't be blank", "Content can't be blank"]
演習2
>> micropost.content = "a"*141 >> micropost.valid? => false >> micropost.errors.full_messages => ["User must exist", "User can't be blank", "Content is too long (maximum is 140 characters)"]
13.1.3
演習1
マイクロポストが保存されました.
>> user = User.first >> micropost = user.microposts.create(content: "Lorem ipsum") (0.3ms) BEGIN Micropost Create (0.5ms) INSERT INTO `microposts` (`content`, `user_id`, `created_at`, `updated_at`) VALUES ('Lorem ipsum', 1, '2018-07-04 04:44:53', '2018-07-04 04:44:53') (0.8ms) COMMIT => #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2018-07-04 04:44:53", updated_at: "2018-07-04 04:44:53">
演習2
検索する場合はid
を渡さなくてはなりません.
>> user.microposts.find(micropost.id) => #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2018-07-04 04:44:53", updated_at: "2018-07-04 04:44:53"> >> user.microposts.find(micropost) ArgumentError (You are passing an instance of ActiveRecord::Base to `find`. Please pass the id of the object by calling `.id`.)
演習3
13.1.2の演習ですでにUser.first
を使ってしまったので, 最後のマイクロポストを取得して比較しました.
>> user == micropost.user => true >> user.microposts.last == micropos => true
13.1.4
演習1, 2
以下のようになりました.
>> Micropost.first.created_at Micropost Load (2.6ms) SELECT `microposts`.* FROM `microposts` ORDER BY `microposts`.`created_at` DESC LIMIT 1 => Wed, 04 Jul 2018 04:44:53 UTC +00:00 >> Micropost.last.created_at Micropost Load (0.8ms) SELECT `microposts`.* FROM `microposts` ORDER BY `microposts`.`created_at` ASC LIMIT 1 => Wed, 04 Jul 2018 04:11:06 UTC +00:00
演習3
ユーザーに紐づけられたマイクロポストの配列を取得する際も, 降順で検索されていることがわかります.
>> user = User.first >> user.microposts Micropost Load (0.5ms) SELECT `microposts`.* FROM `microposts` WHERE `microposts`.`user_id` = 1 ORDER BY `microposts`.`created_at` DESC LIMIT 11
ユーザーを削除すると, マイクロポストも削除されます.
>> user.destroy >> Micropost.find(1)ActiveRecord::RecordNotFound (Couldn't find Micropost with 'id'=1)
13.2 マイクロポストを表示する
13.2.1
1つのマイクロポストを表示するパーシャルには, micropost
というローカル変数が使われています.
# app/views/microposts/_micropost.html.erb <li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <span class="user"><%= link_to micropost.user.name, micropost.user %></span> <span class="content"><%= micropost.content %></span> <span class="timestamp"> Posted <%= time_ago_in_words(micropost.created_at) %> ago. </span> </li>
ではこのローカル変数はどこで定義されているのでしょうか. RailsガイドやAPIを見ると, 以下はすべて等価であることがわかります. ただしインスタンス変数@customer
は, Customer
モデルのインスタンスであることとします.
<%= render @customer %>
<%= render partial: "customer", object: @customer %>
<%= render partial: "customer", locals: { customer: @customer } %>
これらはapp/views/customers/_customer.html.erb
をレンダリングします. またこのパーシャル内ではパーシャル名の先頭からアンダースコアを除いたローカル変数が利用できます. つまりcustomer
というローカル変数が定義されるということです. この場合は
customer = @customer
のような定義がなされます.
Micropostの場合がまさにこの例の通りでしたね. 命名規則によって, シンプルに書くことができるようになっています.
演習1
いろいろ試してみました.
>> helper.time_ago_in_words(3.seconds.ago) => "less than a minute" >> helper.time_ago_in_words(3.minutes.ago) => "3 minutes" >> helper.time_ago_in_words(3.hours.ago) => "about 3 hours" >> helper.time_ago_in_words(3.days.ago) => "3 days" >> helper.time_ago_in_words(3.weeks.ago) => "21 days" >> helper.time_ago_in_words(3.months.ago) => "3 months" >> helper.time_ago_in_words(3.years.ago) => "about 3 years"
演習2
単数が複数かによって多少挙動が異なりますね.
>> helper.time_ago_in_words(1.second.ago) => "less than a minute" >> helper.time_ago_in_words(1.minute.ago) => "1 minute" >> helper.time_ago_in_words(1.hour.ago) => "about 1 hour" >> helper.time_ago_in_words(1.day.ago) => "1 day" >> helper.time_ago_in_words(1.week.ago) => "7 days" >> helper.time_ago_in_words(1.month.ago) => "about 1 month" >> helper.time_ago_in_words(1.year.ago) => "about 1 year"
演習3
以下の通りです.
>> microposts = User.first.microposts.paginate(page: nil) >> microposts.class => Micropost::ActiveRecord_AssociationRelation
直接取得するとこうなります.
>> m = Micropost.all >> m.class => Micropost::ActiveRecord_Relation
13.2.2
演習1, 2
>> (1..10).to_a.take(6) => [1, 2, 3, 4, 5, 6] >> (1..10).take(6) => [1, 2, 3, 4, 5, 6]
演習3
>> Faker::University.name => "The Harris College" >> Faker::University.name => "West Bergstrom Institute" >> Faker::PhoneNumber.phone_number => "1-252-219-7186" >> Faker::PhoneNumber.cell_phone => "1-248-219-0040" >> Faker::Hipster.words(4) => ["DIY", "knausgaard", "kitsch", "pitchfork"] >> Faker::Hipster.sentence => "Poutine butcher salvia green juice vhs biodiesel." >> Faker::ChuckNorris.fact => "The only pattern Chuck Norris knows is God Object." >> Faker::ChuckNorris.fact => "Chuck Norris doesn't need a java compiler, he goes straight to .war"
他にも多種多様なFakerが用意されています. ドキュメントを読むだけでも楽しいですね.
13.2.3
演習1, 2
簡単なので省略します.
13.3 マイクロポストを操作する
13.3.1
演習1
ブログ等を見てみると, 単にDRYに反するから, という解答でいいんですね. メソッドのオーバーライドという仕組みがあるので, コード自体に不備があるというわけではないですね. DRYに反していることが不備なのかもしれませんが...
13.3.2
エラーメッセージパーシャルにオブジェクトを渡すために,
object: f.object
という書き方が見られます. でもおかしく感じる点がありますね. object
キーワードは13.2.1でも登場しましたが, その時とは異なる点があります. それはパーシャル内におけるローカル変数の名前です.
たとえば
render 'micropost', object: @new_micropost
のような書き方をすれば, パーシャル内に定義されるローカル変数はmicropost
です.
しかしエラーメッセージパーシャルの中では, object
という名前の変数を使っていますね.
<% if object.errors.any? %> <div id="error_explanation"> <div class="alert alert-danger"> The form contains <%= pluralize(object.errors.count, "error") %>. </div> <ul> <% object.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %>
パーシャル名と同じ名前のローカル変数は定義されていないのでしょうか. では実験してみましょう. print
デバッグのような方法を取りましょう. エラーメッセージパーシャルの中に, 以下の一行を加えます.
<%= p error_messages %>
パーシャル名と同名の変数の中身を見てやろうということです. これで不正な値をフォームから送ろうと思ってページ遷移したところ, エラーが出ました.
undefined local variable or method `error_messages'
やっぱりダメでしたね.
ということはrender
メソッドの挙動をまだよく理解できていないということです. 詳しくはまた今度調べます.
演習1
以下のようにパーシャルを作成しました.
# app/views/static_pages/home.html.erb <% if logged_in? %> <%= render 'home_logged_in' %> <% else %> <%= render 'welcome' %> <% end %>
# app/views/static_pages/_welcome.html.erb <div class="center jumbotron"> <h1>Welcome to the Sample App</h1> <h2> This is the home page for the <a href="https://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"), 'http://rubyonrails.org/' %>
# app/views/static_pages/_home_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>
13.3.3
演習1
以下の通りです.
INSERT INTO `microposts` (`content`, `user_id`, `created_at`, `updated_at`) VALUES ('Good morning.', 1, '2018-07-05 01:07:23', '2018-07-05 01:07:23')
演習2
すべて等しいです.
>> Micropost.where("user_id=?", user.id) == user.microposts => true >> Micropost.where("user_id=?", user.id) == user.feed => true
13.3.4
演習1, 2
省略.
13.3.5
演習1
無効な送信に対しては, 以下をコメントアウトしました.
# app/models/micropost.rb class Micropost < ApplicationRecord belongs_to :user default_scope -> { order(created_at: :desc) } validates :user_id, presence: true # validates :content, presence: true, length: { maximum: 140 } end
たしかにテストは失敗します.
有効な送信に対しては, 以下をコメントアウトしました.
# app/controllers/microposts_controller.rb def create @micropost = current_user.microposts.build(micropost_params) # if @micropost.save # flash[:success] = "Micropost created!" # redirect_to root_url # else # @feed_items = [] # render 'static_pages/home' # end end
たしかにテストは失敗します.
投稿の削除に対しては, 以下をコメントアウトしました.
# app/controllers/microposts_controller.rb def destroy #@micropost.destroy flash[:success] = "Micropost deleted" redirect_back(fallback_location: root_url) end
確かにテストは失敗します.
異なるユーザのプロフィールに対しては, コメントアウトというか, 以下のように編集しました.
# app/views/microposts/_micropost.html.erb <li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <span class="user"><%= link_to micropost.user.name, micropost.user %></span> <span class="content"><%= micropost.content %></span> <span class="timestanp"> Posted <%= time_ago_in_words(micropost.created_at) %> ago. <% if true #current_user?(micropost.user) %> <%= link_to "delete", micropost, method: :delete, data: { confirm: "You Sure?" } %> <% end %> </span> </li>
確かにテストは失敗します.
演習2
省略.
13.4 マイクロポストの画像投稿
13.4.1
Gemfileを編集したら, Dockerイメージに反映させます.
$ docker-compose build
これでイメージが更新され, 同時にGemもインストールされます. あとはいつも通りコンテナを起動すればOKです.
$ docker-compose up -d
演習1
省略.
演習2
テストコードは以下のように編集しました.
# test/integration/microposts_interface_test.rb test "micropost interface" do log_in_as(@user) get root_path assert_select 'div.pagination' assert_select 'input[type=file]' # 変更点 # invalid post assert_no_difference 'Micropost.count' do post microposts_path, params: { micropost: { content: "" } } end assert_select 'div#error_explanation' # valid post 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 @user.microposts.first.picture? # 変更点 assert_not flash.empty? assert_redirected_to root_url follow_redirect! assert_match content, response.body # 省略 end
13.4.2
チュートリアルの中ではextension_white_list
というメソッドが使われていますが, 自分のファイルを見ると, extension_whitelist
というメソッドでした. アンダースコアの分だけ異なります.
carrierwaveのドキュメントには後者が載っていました. バージョンが上がったことにより名前が変わったんでしょう.
演習1
そもそもそんな大きな画像ねーよと思いましたが, 10mb 画像
と検索したら出てきました.
jQueryによるアラートは出ましたが, 投稿自体はできてしまいました. Railsサーバーを再起動したところ, サーバー側のエラーが表示されるようになりました.
演習2
そもそもブラウザからファイルを指定する場合, 許可された拡張子を持つファイルしか選択できませんね.
13.4.3
Debian環境を使っているので, パッケージのインストールは以下のコマンドで行いました. パッケージ名は全て小文字でOKです.
apt-get install -y imagemagick
Dockerイメージに反映させたい場合は, Railsコンテナ用のDockerfileを編集します.
FROM ruby:2.5.1 RUN apt-get update -qq && apt-get -y install \ vim \ build-essential \ libpq-dev \ nodejs \ mysql-client \ imagemagick # ここを追加! RUN mkdir /myapp COPY Gemfile /myapp COPY Gemfile.lock /myapp WORKDIR /myapp RUN gem install bundler && bundle install ENV COLUMNS=200 LINES=50
DockerfileやGemfileを変更した場合は以下のコマンドを実行します.
$ docker-compose build
あとはいつも通りコンテナを起動すればOKですね.
$ docker-compose up -d
演習1
省略.
演習2
エラー出ない...
rails.png
は小さい画像ですから, 問題なくアップロードされてしまいますよね. だからエラーは出なくて正解な気がしてしまうのですが. とはいえテスト環境ではリサイズが行われないようにしておきます.
13.4.4
この節はやっていません.
Railsチュートリアル 第12章
12.1 PasswordResets リソース
12.1.1
演習1
省略.
演習2
メールにはフルのURLを記載しなくてはなりませんからね.
12.1.2
演習1
APIやRailsガイド等を見てみましたが, よくわかりませんでした. 今回の場合は,
- コントローラ内でインスタンス変数を用意していないから
という理由しか思い浮かびませんでした.
12.1.3
演習1
UserMailer.password_reser
に対し, 以下のエラーが表示されました.
wrong number of arguments (given 1, expected 0)
11章をやっていない場合はまた異なるエラーが出るんでしょうね.
演習2
たしかに演習1で入力したメールアドレスに対応するユーザーには, reset_sent_at
属性に値が存在しました.
12.2 パスワード再設定のメール送信
12.2.1
演習1, 2, 3
省略.
12.2.2
演習1
問題なくクリアしました.
演習2
省略.
12.3 パスワードを再設定する
12.3.1
hidden
タグを利用することによって, update
アクションからもメールアドレスがparams[:email]
という形で参照できるようになりました. edit
アクションとupdate
アクションにおけるメールアドレスの参照の仕方が統一されたため, before action
をつくることができます.
演習1
問題なく表示されました.
演習2
すでに空っぽのupdate
アクションを作っていたので,
No template found for PasswordResetsController#update
というエラーが発生しました.
12.3.2
演習1, 2
省略.
12.3.3
演習1
省略.
演習2
該当箇所は以下のように記述しました.
assert_nil user.reload.reset_digest
演習3
省略.
演習4
update
アクションを以下のように変更しました.
# app/controllers/password_resets_controller.rb def update if params[:user][:password].empty? @user.errors.add(:password, :blank) render 'edit' elsif @user.update_attributes(user_params) log_in(@user) @user.update_attribute(:reset_digest, nil) # 追加 flash[:success] = 'Password has been reset' redirect_to @user else render 'edit' end end
12.4 本番環境でのメール送信
この節はやっていません.
Railsチュートリアル 第11章
11.1 AccountActivations リソース
11.1.1
演習1
省略.
演習2
メールにはフルのURLが記載されていないとアクセスできませんからね.
11.1.2
演習1
省略.
演習2
インスタンスを生成するのは面倒ですが, before_create
が機能しているかを確かめましょう. create_activation_digest
を呼び出した際のログは一部省略しています.
>> user = User.create( >* name: "Console Test", >* email: "console@test.com", >* password: "password", >* password_confirmation: "password") >> user.create_activation_digest NoMethodError (private method `create_activation_digest' called for #<User:0x00007fbb70839d60>) >> user.activation_digest => "$2a$10$olzQ9tgFiKu/mrHwP/D5nO8XVyiz1TJ9dSVKO1.lvkc9EAdnSL6cK"
はじめbefore_create
の機能を勘違いしていました. Rubyとしてのオブジェクトを作成する前に実行されるものだと思っていました. だから上の演習もUser.new
を実行して確認しようとしていました.
しかしここでいうcreate
は, データベース操作CRUDのうちのひとつのことだったんですね. だからUser.new
ではactivation_token
が作成されませんでした.
11.2 アカウント有効化のメール送信
11.2.1
演習1
以下のようになりました.
>> CGI.escape("Don't panic!") => "Don%27t+panic%21"
11.2.2
config/environments/development.rb
を編集したあとの再起動は以下のコマンドで行いました.
$ docker-compose restart
ちなみに再起動する前にメールプレビューを見てみましたが, エラーが出ました.
演習1
Date
欄には以下の内容が表示されました.
Mon, 02 Jul 2018 04:59:25 +0000
Locale
を変更しても特に変化はありませんでしたね.
11.2.3
演習1, 2
省略.
11.2.4
演習1
Railsサーバーが起動しているコンテナの標準入出力をローカルマシンのターミナルにアタッチします.
$ docker attach <container id or name>
この状態で新規ユーザー登録をすると, メール送信のログを見ることができます.
終わったら
Ctrl+P, Ctrl+Q
を押してデタッチをしましょう. 間違ってCtrl+D
等でコンテナを抜け, コンテナが落ちてしまった場合はこちらを参考にしてください.
演習2
省略.
11.3 アカウントを有効化する
11.3.1
演習1, 2
こんな感じです.
>> user = User.create( >* name: "Activation Test", >* email: "activation@test.com", >* password: "password", >* password_confirmation: "password") >> token = user.activation_token => "Pr4fTMDEgwnsh4rTUSsQiA" >> user.authenticated?(:activation, token) => true
11.3.2
演習1, 2
省略.
11.3.3
演習1, 2
簡単なので省略.
演習3
アクティベートされていないユーザーをfixture
内に作成しました.
# test/fixtures/users.yml non_activated: name: Non Activated email: non_active@example.com password_digest: <%= User.digest('password') %>
統合テスト用の新しいクラスを作成すべきなのか, 不勉強なため判断がつきませんでした. すくなくともusers/index
ページには関連したテストなので, users_index_test.rb
に書くことにしました.
# test/integration/users_index_test.rb def setup @admin = users(:michael) @non_admin = users(:archer) @non_activated = users(:non_activated) end test "should not show non-activated user" do log_in_as(@admin) get users_path assert_select 'a[href=?]', user_path(@non_activated), count: 0 get user_path(@non_activated) assert_redirected_to root_url end
11.4 本番環境でのメール送信
この節はやっていません.
Railsチュートリアル 第10章
10.1 ユーザーを表示する
10.1.1
演習1
省略.
演習2
new.html.erb
とedit.html.etb
のフォームはほとんど同じ形をしているから, パーシャルでまとめてしまおうということですね. ほとんど同じとはいっても異なる点が2箇所あります. それがボタンのテキストと, フォームのアクション属性です. これらを考慮しながらパーシャルを以下のように作りました.
# app/views/users/_form.html.erb <%= form_for(@user, url: yield(:form_action)) do |f| %> <%= render 'shared/error_messages' %> # 気になった点その1 <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit yield(:button_text), class: 'btn btn-primary' %> <% end %>
# app/views/users/new.html.erb <% provide(:title, "Sign up") %> <% provide(:button_text, 'Create my account') %> <% provide(:form_action, signup_path) %> <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(:button_text, 'Save changes') %> <% provide(:form_action, user_path) %> # 気になった点その2 <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>
ただしいくつか気になった点があったのでそれらを掘り下げていくことにします.
気になった点その1
気になった箇所はコード内にコメントしておきました. この行の内容は, チュートリアルの見本コードとは異なります. なぜかというと不要ではないかと感じたからです. まずチュートリアルのコードを提示しましょう.
<%= render 'shared/error_messages', object: @user%>
render
メソッドにハッシュが渡されているのがわかりますね. これが必要かどうかを議論するには, render
メソッドの使い方について理解しなければなりません. 詳しくはAPI)を見ていただきたいのですが, ここでは上のコードにフォーカスして説明します.
render
メソッドには, パーシャルに変数を渡す方法が用意されています. つまりテンプレート内で使用可能な変数を, パーシャル内の変数に割り当てることができます. その方法のうちのひとつが, object:
という記法です. Rubyではすべてがオブジェクトですから, :object
というキーが使われているんだと思います.
見ての通り@user
というUser
オブジェクトをパーシャルに渡していることがわかります. ではパーシャル内ではどのような変数名でこのオブジェクトにアクセスできるのでしょうか.
たとえば
object: @user, as: 'user'
のように書けば, パーシャル内では変数user
にUser
オブジェクトが格納されることになります. つまりパーシャル内の変数名をas:
によって指定できるわけですね.
しかしチュートリアルではこの指定が無いのでデフォルトの変数名が指定されます. それはパーシャル名です. ここで言えばerror_messages
ですね.
render
メソッドの説明が済んだので結論です. チュートリアルのコードでは, パーシャル_error_messages.html.erb
内において, オブジェクト@user
が変数error_messages
に格納されるようになっています. しかし変数error_messages
は使われていません. そのかわり直接インスタンス変数@user
を参照しています. したがってobject: @user
は不要であると考えられます.
気になった点その2
これも気になった箇所はコード内にコメントしておきました. この行ははじめ以下のように書きました.
<% provide(:form_action, user_path(@user)) %>
表7.1を見ても, このような記法がなされています. しかし同じ演習問題の解答を載せているブログ等を見てみると,
<% provide(:form_action, user_path) %>
のように書かれています. User
オブジェクトがなくても正しいURLになるのか不安でした.
このコードでよい理由はおそらく, パーシャルを適用させたテンプレートがこのようになるからでしょうね.
form_for(@user, url: user_path)
form_for
メソッドの第一引数に@user
を渡している時点で, ユーザーID等の情報がフォームに伝わっています. だからuser_path
に@user
を渡さなくとも, 正しいURLを生成できるということなのでしょう.
10.1.2
省略.
10.1.3
演習1
以下の一行を追加しました.
# test/integration/users_edit_test.rb assert_select "div.alert", "The form contains 4 errors."
10.1.4
省略.
10.2 認可
10.2.1
beforeフィルターを次のように定義しています.
def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end
login_path
ではなく, login_url
としているのがポイントですね. これについては第5章に書かれています.
なお、Railsチュートリアルでは一般的な規約に従い、基本的には
_path
書式を使い、リダイレクトの場合のみ_url
書式を使うようにします。これはHTTPの標準としては、リダイレクトのときに完全なURLが要求されるためです。演習1
beforeフィルターが全コントローラに適用された場合は, SignupページやLoginページにアクセスできなくなりますから, 多くのテストが失敗しますね.
10.2.2
演習1
調べてみると,
curl
コマンドでPATCH
メソッドのHTTPリクエストを送れるようですね. ということは, フォーム画面=edit
アクションを経由することなく, いきなりupdate
アクションにデータを送信できるということです. だからupdate
アクションに対してもbeforeフィルターが必要ということですね.
演習2
ブラウザで確認しやすいのはedit
アクションのほうですね.
10.2.3
フレンドリーフォワーディングのテストは過剰な気がします.
- 編集画面にアクセス
- ログイン画面にリダイレクト
- ログイン
- さっきの編集画面にリダイレクト
という流れをチェックするためのテストなので, データが正しく更新されたかどうかをチェックする部分は不要なのではないでしょうか. 更新がうまくいくかのテストはすでに書いているわけですしね. もちろん手厚いテストを書いておけばいいのはわかりますが, なくてはならないのかが気になります.
演習1
はじめはsuccessful edit with friendly forwarding
テストの最後に以下を追加することを考えました.
delete logout_path
get login_path
assert_nil session[:forwarding_url]
つまり一旦ログアウトして, またログインするときにはデフォルトページにリダイレクトされるということを確認しています. しかし本文を見返してみると, この演習について以下のように書かれています.
session.delete(:forwarding_url)
という行を通して転送用のURLを削除している点にも注意してください。これをやっておかないと、次回ログインしたときに保護されたページに転送されてしまい、ブラウザを閉じるまでこれが繰り返されてしまいます (このコードのテストは10.2.3.1の演習に回します)。
というわけで, ちゃんとsession
の内容がデリートされているかをチェックすればいいんですね. チュートリアルをこなしているブログを参考にすると, 以下のようなテストがよさそうです. session
の中身をログインの前後で確認しています.
test "successful edit with friendly forwarding" do get edit_user_path(@user) assert_equal edit_user_url(@user), session[:forwarding_url] #追加 log_in_as(@user) assert_nil session[:forwarding_url] #追加 assert_redirected_to edit_user_url(@user) name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email }} assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end
演習2
session[:forwarding_url]
とrequest.get?
はそれぞれ期待通りの値が返ってきました.
(byebug) session[:forwarding_url] "http://localhost/users/1/edit" (byebug) request.get? true
せっかくだから他のことも調べておこうと思ったら...あれ?
(byebug) request.original_url "http://localhost/login"
session[:forwarding_url]
の内容と異なりますね. つまりログインページにリダイレクトした際にRequest
オブジェクトが更新されたということです. APIを参考にこのRequest
オブジェクトを少し調べてみましょう.
(byebug) request.class ActionDispatch::Request (byebug) request.controller_class SessionsController (byebug) request.request_parameters {}
これだけやればこの演習はよいでしょう.
10.3 すべてのユーザーを表示する
10.3.1
演習1
以下のように書いてみました.
def setup @user = users(:michael) end test "layout links when logged in" do log_in_as(@user) get root_path assert_select "a[href=?]", root_path, count: 2 assert_select "a[href=?]", help_path assert_select "a[href=?]", about_path assert_select "a[href=?]", contact_path assert_select "a[href=?]", user_path(@user) assert_select "a[href=?]", edit_user_path(@user) assert_select "a[href=?]", logout_path end
10.3.2
rails db
コマンドに時間は全くかかりませんでした.
演習1
Example Userでログインした状態でusers/2/edit
へアクセスすると, トップページにリダイレクトされました.
10.3.3
演習1
省略.
演習2
どちらもUsers::ActiveRecord_Relation
クラスでした.
10.3.4
演習1
Redにならないぞと思ったら, コメントアウトを#
でやってました. 正しくコメントアウトしたところ, テストは失敗しました.
演習2
<div class="pagination">
が2つあることを確認するために, count: 2
を書き加えました.
test "index including pagination" do log_in_as(@user) get users_path assert_template 'users/index' assert_select 'div.pagination', count: 2 # 追加 User.paginate(page: 1).each do |user| assert_select 'a[href=?]', user_path(user), text: user.name end end
10.3.5
演習1
省略.
10.4 ユーザーを削除する
10.4.1
演習1
以下のように実装しました.
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, admin: true }} assert_not @other_user.reload.admin? end
10.4.2
演習1
以下のコマンドにより, Railsサーバーが稼働しているコンテナの標準入出力を手元のターミナルにアタッチします.
docker attach <container id or name>
この状態でユーザーを削除すると, 以下のようなログが表示されました.
Started DELETE "/users/6" for 172.18.0.1 at 2018-07-01 07:45:23 +0000 Cannot render console from 172.18.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#destroy as HTML Parameters: {"authenticity_token"=>"tBX07b8P1vb9hze5S0SicpXXW67+E235kSq1EkOgpDF4AXJuJmwVI98KZmGquu8Eq8T42/5HkjSBLSor8YarBg==", "id"=>"6"} User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 ↳ app/helpers/sessions_helper.rb:18 User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 6 LIMIT 1 ↳ app/controllers/users_controller.rb:44 (0.3ms) BEGIN ↳ app/controllers/users_controller.rb:44 User Destroy (0.5ms) DELETE FROM `users` WHERE `users`.`id` = 6 ↳ app/controllers/users_controller.rb:44 (1.2ms) COMMIT ↳ app/controllers/users_controller.rb:44 Redirected to http://localhost/users Completed 302 Found in 15ms (ActiveRecord: 3.0ms) Started GET "/users" for 172.18.0.1 at 2018-07-01 07:45:23 +0000 Cannot render console from 172.18.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#index as HTML User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 ↳ app/helpers/sessions_helper.rb:18 Rendering users/index.html.erb within layouts/application (0.7ms) SELECT COUNT(*) FROM `users` ↳ app/views/users/index.html.erb:4 User Load (0.4ms) SELECT `users`.* FROM `users` LIMIT 30 OFFSET 0 ↳ app/views/users/index.html.erb:7 Rendered collection of users/_user.html.erb [30 times] (4.8ms) Rendered users/index.html.erb within layouts/application (22.6ms) Rendered layouts/_rails_default.html.erb (107.4ms) Rendered layouts/_shim.html.erb (0.3ms) Rendered layouts/_header.html.erb (1.1ms) Rendered layouts/_footer.html.erb (0.4ms) Completed 200 OK in 194ms (Views: 180.8ms | ActiveRecord: 1.6ms)
10.4.3
演習1
省略.
Railsチュートリアル 第9章
9.1 Remember me 機能
9.1.1
演習1
問題なく動作しました.
演習2
もともとリスト9.4の書き方をしていました. リスト9.5の書き方は省略します.
9.1.2
疑問1 current_user
メソッドの定義
第8章では一時セッションにユーザーIDを保存していましたが, 本章ではユーザー情報を永続的に保存するために, クッキーを用いています. そのため, 第8章で定義したcurrent_user
メソッドを修正しています. それがこちらです.
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]) log_in user @current_user = user end end end
はじめはsession
とcookies
の評価がどうしてこの順番なのかがわかりませんでした.
コードを見た通り, session
が空かどうかをチェックし, 空であった場合はcookies
が空かどうかのチェックに移っています. この評価の順番が正当化されるということは, 「session
は空だがcookies
は空ではない」という状況がありえることになります. それはブラウザを閉じたあとのことですね. session
は短命なのでブラウザを閉じれは消えてしまいます. しかしcookies
は残ったままです. これでこの順番に評価をすることが意味を持つわけですね.
また後に実装するように, ユーザーはRemember me
機能を使わないという選択をとれるようになります. したがってユーザーによってはcookies
には値がない場合もあるわけです. それならば必ず利用されるsession
をはじめに評価するほうが合理的ですね.
疑問2 どうしてログアウトできないのか
演習の直前に, テストスイートが失敗することが述べられています. その理由として, ユーザーがログアウトできないからだと書いてあります. 今までに定義したヘルパーメソッド等を思い出しながら, これについて詳しく見ていきましょう.
実際に統合テストを実行すると失敗します. 失敗した箇所を以下に示します.
# test/integration/users_login_test.rb # login_with_valid_information_followed_by_logout # ログアウトの処理以降のみを抜粋 delete logout_path assert_not is_logged_in? assert_redirected_to root_url follow_redirect! assert_select "a[href=?]", login_path #ここで失敗 assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0
以下テストで実行される流れを書いていきます.
logout_path
にdelete
リクエストが飛びます. これによりSessionsController
のdestroy
アクションが実行されます.
# SessionsController def destroy log_out redirect_to root_url end
ログアウト処理がなされ, root_url
にリダイレクトしていますね. このログアウト処理はなにをしているかというと, session
からユーザーIDを消去し, インスタンス変数の@current_user
を初期化しています.
# app/helpers/sessions_helper.rb def log_out session.delete(:user_id) @current_user = nil end
テストの続きへ進みます.
テストヘルパーメソッドのis_logged_in?
によって, ログインした状態かどうかをチェックしています.
def is_logged_in? !session[:user_id].nil? end
log_out
メソッドによりsession
の内容は消去しているので, 当然このアサーションはクリアします. 次です.
destroy
アクションの定義内に, リダイレクトするように書かれていますから, redirected_to
アサーションもクリアします. 問題は次です.
さて問題はselect
アサーションです. これが失敗する理由は本文にもあるようにログアウトできていないからです. つまりログインしてしまっているからです. ちゃんとsession
を消去したのに, 復活してしまっているんですね. その答えがヘッダー内の次の一行にあります.
# app/views/layouts/_header.html.erb <% if logged_in? %>
ログインしているかどうかによって, ヘッダーの内容を変化させるためのif
文です. ではヘルパーメソッドのlogged_in?
の定義を見てみましょう.
# app/helpers/sessions_helper.rb def logged_in? !current_user.nil? end
current_user
がnil
かどうかで, ログインしているかどうかを判断しています. さてcurrent_user
メソッドはどんな定義でしたっけ...? 疑問1でも取り上げたように, 以下のような定義でしたね.
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]) log_in user @current_user = user end end end
そろそろ核心に近づいてきました. current_user
メソッドでは, session
が空であった場合はcookies
の中身を見ます. ログアウトしてsession
情報を消去しtも, cookies
の中身は残ったままですよね. ということはそのデータをもとにしてユーザーの検索が行われます. そしてauthenticated?
が通った場合, log_in
メソッドが実行されてしまいます. ここが, session
情報の復活点でした. cookie
情報が残った状態でヘッダーのレンダリングを行えば, 自然とログインしてしまうようになっていたのです.
演習1, 2
省略.
9.1.3
これでログアウトができますね.
演習1
省略.
9.1.4
細かいnil
の扱いは難しいですね...
演習1, 2, 3
省略.
9.2 [Remenber me] チェックボックス
省略.
9.3 [Remember me] のテスト
9.3.1
演習1
sessions_controller.rb
のcreate
内のuser
をインスタンス変数@user
に変えます. するとassigns
メソッドでインスタンス変数を参照できるようになるので, より詳細にテストを書くことができますね.
# test/integration/users_login_test.rb test "login with remembering" do log_in_as(@user, remember_me: '1') assert_equal cookies['remember_token'] , assigns(:user).remember_token end
9.3.2
省略.
Railsチュートリアル 第8章
8.1 セッション
8.1.1
演習1
省略.
演習2
routes.rb
に記述したルーティングのみが表示されました.
8.1.2
演習1
routes.rb
における設定
post '/login', to: 'sessions#create'
により実現されていますね.
8.1.3
演習1
省略.
8.1.4
Flashメッセージについて今ひとつよくわからなかったので, Railsガイドを参照しました.
flashはセッションの中の特殊な部分であり、リクエストごとにクリアされます。この特徴から、flashは「直後のリクエスト」でのみ参照可能になります。
これを踏まえて, Flashメッセージが残ってしまう理由を説明しましょう.
ログインボタンを押すことにより, /login
に対しPOSTリクエストが送られます. このとき変数flash
にエラーメッセージが格納されます.
これと同時にnew
ページがレンダリングされますが, これは同一のPOSTリクエストの中で行われます.
上の引用文からもわかるように, 次のリクエストでもまだ変数flash
にエラーメッセージは格納されたままです. だからHomeページに移動してもフラッシュメッセージが表示されたままなのです.
次にHelpページなどにアクセスし, もう一度HTTPリクエストを送ることにより, やっとflash
の中身がクリアされます.
8.1.5
演習1
flash.now
がうまく機能していることがブラウザ上でも確認できます.
8.2 ログイン
Applicationコントローラの定義内に, 追加した覚えのないメソッドがある...どこかで見落としたのでしょうか.
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base protect_from_forgery with: :exception # 見知らぬ行 include SessionHelper end
8.2.1
演習1
Sxi%2FKPGHw...
のような暗号化された値で保存されていますね.
演習2
有効期限の欄にはセッション
と書かれています.
8.2.2
演習1
たしかにnil
が返ってきます.
irb(main):003:0> User.find_by(id: 2) User Load (0.9ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1 => nil
演習2
以下のようになりました. 例外を発生させないことと, 2回目にデータベースへの問い合わせが無いことが確認できますね.
>> session = {} => {} >> session[:user_id] = nil => nil >> @current_user ||= User.find_by(id: session[:user_id]) (0.6ms) SET NAMES utf8, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483 User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1 => nil >> session[:user_id] = User.first.id User Load (0.5ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1 => 1 >> @current_user ||= User.find_by(id: session[:user_id]) User Load (1.2ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 => #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2018-06-28 05:35:56", updated_at: "2018-06-28 05:35:56", password_digest: "$2a$10$i8HBze1p.5miIytb9ahZjefM27b8EXVMSMjV/B0za7t..."> >> @current_user ||= User.find_by(id: session[:user_id]) => #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2018-06-28 05:35:56", updated_at: "2018-06-28 05:35:56", password_digest: "$2a$10$i8HBze1p.5miIytb9ahZjefM27b8EXVMSMjV/B0za7t...">
8.2.3
Bootstrapのドロップダウンがうまく機能しません. 何行かコードを追加すると動くという記事もありましたが, 浅い理解でコードを書き換えるのも嫌なので後回しにします.
と思ったらただのタイポでした. ではBootstrapが動かないと報告している記事はなんだったんでしょう.
演習1, 2
省略.
8.2.4
演習1, 2
省略.
8.2.5
演習1, 2
省略.
8.3 ログアウト
演習1
省略.
演習2
ログアウトと同時にcookieの値が変化していることがわかります.