kuma0319のブログ

webエンジニアになるためのアウトプットブログ

Ruby on Railsチュートリアル第13章

Railsチュートリアルの第13章を進めていきます。

第13章(ユーザーのマイクロポスト)

Micropostモデル

マイグレーション

マイクロポストを生成する際はrails generate model Micropost content:text user:referencesとして、user:referencesを引数に含める。これによって、マイグレーションファイルにt.references :user, null: false, foreign_key: trueという行が追加される。これは、foreign_key: trueによって外部キー制約が課され、マイクロポストがusersテーブルのidを参照し、自動的にuser_idというカラムを作成する。
さらに、add_index :microposts, [:user_id, :created_at]というインデックスカラム(ここでは両方のキーを同時に持つ複合キーインデックス)を付与することで、user_idに関連付けられたマイクロポストを順序だてて取り出せる。

カラム名 データ型
id integer
content text
user_id integer
created_at datetime
updated_at datetime
User/Micropost関連付け

Userはhas_manyで0以上の複数のマイクロポストを持っており、Micropostはbelongs_toで必ず1人のユーザーに属している。そのため、マイクロポストに対するアクションは、Micropost.createではなくuser.microposts.createのように、ユーザーと関連付けを経由したマイクロポストが作成される。
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)は、@micropost = @user.microposts.build(content: "Lorem ipsum")と記述するのが正しい。

newとbuildについて

newとbuildはどちらもオブジェクトを生成するメソッドで、buildはnewのエイリアスとなっているようで、機能としては全く同一(もともとはこうでは無かったようです)。
慣習的に今回のように、モデルに関連付けられたオブジェクトを生成する場合に使用するらしい。

デフォルトスコープ

マイクロポストの読み出しは順序に対する保証は何もないが、これを順序立てて表示するのがdefault scopeというテクニック。
この機能はアプリケーション側の実装が間違っていてもテストが成功するという問題に陥りやすい部分。
default_scope -> { order(created_at: :desc) }という記述であれば、created_atの降順(desc)で並び替えが可能。デフォルトスコープのデフォルトは昇順。

fixture

デフォルトスコープをテストするためのfixtureでは、以下のようにERB形式でcreated_atを更新できる。また、user:michaelと指定すると、ユーザーfixtureのmichaelのマイクロポストであることを指示可能。

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael
関連付けられたオブジェクトの削除

has_many :micropostshas_many :microposts, dependent: :destroyと指定すると、ユーザー削除された際にマイクロポストも削除されるようになる。

マイクロポストの表示

10章のユーザー一覧を表示したり、ページネーションしたりした部分と酷似している。
マイクロポスト一覧を表示するshowアクションは、ユーザーとマイクロポストとの関連付けを経由してマイクロポストの情報をpaginateしている。

[app/controllers/users_controller.rb]
  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.paginate(page: params[:page])
  end

マイクロポストの操作

マイクロポストリソースに対するコントローラのアクションは、createとdestroyがあれば十分。resources :microposts, only: [:create, :destroy]
マイクロポストはnew画面を作成しない代わりに、ルートパス(home)にマイクロポスト投稿のフォームを配置する。

createアクション

ユーザーのcreateアクション@user = User.new(user_params)と似ており、@micropost = current_user.microposts.build(micropost_params)でオブジェクトを生成している。ここでは、現在のユーザーを返すcurrent_userに紐づけて、関連付けをおこなう生成メソッドなのでnewではなくbuildとしている。更に、micropost_paramsでcontent属性のみを許可するStrong Parametersとしている。

[app/controllers/microposts_controller.rb]
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      render 'static_pages/home', status: :unprocessable_entity
    end
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end
homeページの分岐処理

homeはログインしているユーザーとそうでないユーザーで表示する内容を変える方が今回のアプリケーションだと適切。

[app/views/static_pages/home.html.erb]
<% if logged_in? %>
  <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>
<% else %>



<% end %>
マイクロポストパーシャル

'shared/micropost_form’に該当するパーシャルは以下の記述。

[app/views/shared/_micropost_form.html.erb]
<%= form_with(model: @micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
form_withに渡す@micropost

第7章のsignup(new)ページでは、form_withに渡す変数@userの情報@user = User.newをnewアクションで定義して渡していた。
今回のform_with(model: @micropost)においても、homeページであるためhomeアクションに@micropostを定義してあげる必要がある。

[app/controllers/static_pages_controller.rb]
  def home
    @micropost = current_user.microposts.build if logged_in?
  end
エラーメッセージのパーシャル

エラーメッセージのパーシャルは、変数@userのエラーを参照することを前提として作成していたが、このままだと@miropostに対するエラーを表示できない。
ユーザーのnewビューにおけるパーシャルの呼出し(変更前)

[app/views/users/new.html.erb]
    <%= form_with(model: @user) do |f| %>
      <%= render 'shared/error_messages' %>

エラーメッセージのパーシャル(変更後)

[app/views/shared/_error_messages.html.erb]
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

この場合は、<%= render 'shared/error_messages', object: f.object %>とするとobject: f.objectのおかげでerror_messagesパーシャルの中に変数objectを作成出来る。これであれば、どのような変数であっても対応できる。

ユーザーのnewビューにおけるパーシャルの呼出し(変更前)

[app/views/users/new.html.erb]
    <%= form_with(model: @user) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>

エラーメッセージのパーシャル(変更後)

[app/views/shared/_error_messages.html.erb]
<% 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 %>
フィード

フィード(タイムライン)はすべてのUserが持つものだからUserモデルで作成。

  def feed
    Micropost.where("user_id = ?", id)
  end

ここで、「?」はプレースホルダと呼ばれ、効果としてはSQLクエリに代入する前にidがエスケープされ、SQLインジェクションを回避できる。SQL文に変数を代入する際は常にエスケープする。
作成したフィードメソッドは、homeアクションに@feed_items = current_user.feed.paginate(page: params[:page])として追加する。

フィードのパーシャル

フィードのパーシャルにおいて、render @feed_itemsが少し難しい。記述の中身は以下の通り

[app/views/shared/_feed.html.erb]
<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>

通常renderメソッドは、render 'shared/error_massages'のようにパーシャルを指定する。今回のような下記方は10章などでrender @userのような書き方も出てきたが、この場合はUserクラスの変数@userを指定しているため自動的に_user.html.erbというパーシャルを自動的に探すため分かりやすい。
今回のrender @feed_items_feed_items.html.erbというパーシャルを探すのではなくマイクロポストのパーシャル_micropost.html.erbを呼び出している。これは、@feed_itemsの属しているクラスがMicropostクラスであるため、そのクラスに該当するパーシャルを該当するディレクトリ「app/views/microposts/_micropost.html.erb」から探す仕組みらしい。
つまりは、指定している変数そのものの名前に由来するパーシャルというよりは、変数が属しているクラスに由来する名前のパーシャルを探すという動作。

ここで疑問なのが、<%= render @feed_items %>の動作がマイクロポストのパーシャルを呼び出す動作であれば、<%= render 'microposts/micropost' %>と直でそのパーシャルを指定した方が読み手も分かりやすいのではないかと思う。
chatGPTさんに聞いてみたところ、「<%= render 'microposts/micropost' %>という形式でも、同じパーシャルを呼び出すことができます。ただし、この場合、@feed_itemsがMicropostの配列であることを明示的に示す必要があります。具体的には、<%= render partial: 'microposts/micropost', collection: @feed_items %>と書く必要があります。ただし、この方法は少し冗長であるため、通常は<%= render @feed_items %>という形式が好まれます。」との回答。

つまり、render @feed_itemsという記述はrender 'microposts/micropost'を指定すると同時に@feed_itemsそのものがMicropostの配列であることを示していることになる。初学者には、冗長でも<%= render partial: 'microposts/micropost', collection: @feed_items %>とした方が一目で理解は出来そう。。。

マイクロポストの削除

検索して削除する。
検索はユーザーの場合はUser.find(params[:id])の形であったがマイクロポストは@micropost = current_user.microposts.find_by(id: params[:id])で関連付けを経由してfind_byメソッドで探している。これによって、別のユーザーが他人のマイクロポストを削除しようとしてもnilが返される。(ユーザー削除は、beforeアクションでadmin_userであることを指定していたから問題ない?)

削除は検索で定義した@micropostを@micropost.destroyで削除する。

リファラ

削除された後の以降ページは特定のページでは無く元のページに戻る方が適切なので、リファラー(referrer)の示す参照ページに飛ぶのが好ましい。フレンドリーフォワーディング機能のrequest.original_urlは、”現在”のリクエストのURLを返すため、ログインしていないユーザーがログインページへリダイレクトされ、ログインした後に保存したURLへ飛ぶ機能。リファラーのrequest.referrerは、現在のリクエストを行う”直前”にユーザーがいたページのURLを返すため、少し異なる。

  def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    if request.referrer.nil?
      redirect_to root_url, status: :see_other
    else
      redirect_to request.referrer, status: :see_other
    end
  end

マイクロポストの画像投稿

Active Strage

メディアのアップロードやアップロードした画像の成形等にも使用可能。 railsguides.jp

  • has_one_attechedオプションでは、Active Recordオブジェクト(マイクロポストなど)1件につき1件のファイル(画像など)が添付可能なオプション。has_many_attachedオプションは複数添付可能。
  • Active Strage APIのattachメソッドでは、アップロードされた画像を@micropostオブジェクトにアタッチ(添付)出来る。この場合、params.requireにimage属性も許可しておくようにする。
  • Active Storageにフォーマット機能やバリデーション機能を付与するには、active_storage_validationsというgemを追加することで対応可能。これによって、メディアタイプやファイルサイズにバリデーションを付けることが出来る。
  • 画像サイズ(縦幅や横幅)に対するバリデーションはImageMagickというプログラムを用いることで可能。

第13章ー演習ー

13.1.1

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

irb(main):004:0> micropost
=> #<Micropost:0x00007fe3ce19bae0 id: nil, content: "Lorem ipsum", user_id: 1, created_at: nil, updated_at: nil>

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

irb(main):005:0> micropost.user
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=>                                                                                  
#<User:0x00007fe3cd995b70                                                           
 id: 1,                                                                             
 name: "Example User",                                                              
 email: "example@railstutorial.org",                                                
 created_at: Wed, 19 Apr 2023 09:56:53.723792000 UTC +00:00,                        
 updated_at: Thu, 20 Apr 2023 07:11:18.310405000 UTC +00:00,                        
 password_digest: "[FILTERED]",                                      
 remember_digest: "$2a$12$RdhZlokywEJZl9Zrg8yPCeskk4HnuQCOSdyhEHYh7WgApSdCOQkJm",
 admin: true,                                                        
 activation_digest: nil,                                             
 activated: true,                                                    
 activated_at: Wed, 19 Apr 2023 09:56:53.455530000 UTC +00:00,       
 reset_digest: "$2a$12$n.ovhYFMzpgkloM0sSDWSO2UYtCqR9Dy9gfKQf/a7YNA/cij7gkk6",
 reset_sent_at: Thu, 20 Apr 2023 06:38:46.390306000 UTC +00:00>

irb(main):006:0> micropost.user.name
=> "Example User"

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

irb(main):007:0> micropost.save
  TRANSACTION (0.5ms)  begin transaction
  Micropost Create (0.8ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "Lorem ipsum"], ["user_id", 1], ["created_at", "2023-04-20 09:15:48.400796"], ["updated_at", "2023-04-20 09:15:48.400796"]]                       
  TRANSACTION (17.7ms)  commit transaction                           
=> true       

irb(main):009:0> micropost
=> #<Micropost:0x00007fe3ce19bae0 id: 1, content: "Lorem ipsum", user_id: 1, created_at: Thu, 20 Apr 2023 09:15:48.400796000 UTC +00:00, updated_at: Thu, 20 Apr 2023 09:15:48.400796000 UTC +00:00>

13.1.2

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

irb(main):001:0> micropost = Micropost.new
=> #<Micropost:0x00007f43fa5df788 id: nil, content: nil, user_id: nil, created_at: nil, updated_at: nil>
irb(main):002:0> micropost.valid?
=> false

irb(main):004:0> micropost.errors.messages
=> {:user=>["must exist"], :user_id=>["can't be blank"], :content=>["can't be blank"]}

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

irb(main):004:0> micropost.errors.messages
=> {:user=>["must exist"], :user_id=>["can't be blank"], :content=>["can't be blank"]}
irb(main):005:0> micropost.content = 'a' *150
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...
irb(main):006:0> micropost.valid?
=> false
irb(main):007:0> micropost.errors.messages
=> 
{:user=>["must exist"],                                              
 :user_id=>["can't be blank"],                                       
 :content=>["is too long (maximum is 140 characters)"]}   

13.1.3

①データベースにいる最初のユーザーを変数userに代入してください。そのuserオブジェクトを使ってmicropost = user.microposts.create(content: "Lorem ipsum")を実行すると、どのような結果が得られるでしょうか?
(解答)user_idも自動的に付与されてuserモデルに関連付けられたマイクロポストが生成された。

irb(main):002:0> micropost = user.microposts.create(content: "Lorem ipsum")
  TRANSACTION (0.1ms)  begin transaction
  Micropost Create (2.7ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "Lorem ipsum"], ["user_id", 1], ["created_at", "2023-04-20 09:52:23.940675"], ["updated_at", "2023-04-20 09:52:23.940675"]]                      
  TRANSACTION (14.6ms)  commit transaction                             
=>                                                                     
#<Micropost:0x00007f460ed27bd8                                         
...                   

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

irb(main):005:0> user.microposts.find(micropost.id)
  Micropost Load (0.3ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? LIMIT ?  [["user_id", 1], ["id", 2], ["LIMIT", 1]]                     
=>                                                                                        
#<Micropost:0x00007f460d72e520                                                            
 id: 2,                                                                                   
 content: "Lorem ipsum",                                                                  
 user_id: 1,                                                                              
 created_at: Thu, 20 Apr 2023 09:52:23.940675000 UTC +00:00,                              
 updated_at: Thu, 20 Apr 2023 09:52:23.940675000 UTC +00:00>   

irb(main):006:0> user.microposts.find(micropost)
/usr/local/bundle/ruby/3.1.0/gems/activerecord-7.0.4/lib/active_record/relation/finder_methods.rb:466:in `find_one': You are passing an instance of ActiveRecord::Base to `find`. Please pass the id of the object by calling `.id`. (ArgumentError)

③user == micropost.userを実行した結果はどうなるでしょうか? また、user.microposts.first == micropost を実行した結果はどうなるでしょうか? それぞれ確認してみてください。
(解答) 前の演習でid1のマイクロポストは作成していたため、id2のマイクロポストならtrueになる。

irb(main):008:0> user.microposts.first == micropost
  Micropost Load (10.0ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."id" ASC LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
=> false                                                       
irb(main):009:0> user.microposts.second == micropost
  Micropost Load (0.5ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."id" ASC LIMIT ? OFFSET ?  [["user_id", 1], ["LIMIT", 1], ["OFFSET", 1]]
=> true   

13.1.4

①Micropost.first.created_atの実行結果と、Micropost.last.created_atの実行結果を比べてみましょう。
(解答)

irb(main):010:0> Micropost.first.created_at
  Micropost Load (37.1ms)  SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."id" ASC LIMIT ?  [["LIMIT", 1]]                                                
=> Thu, 20 Apr 2023 09:15:48.400796000 UTC +00:00              
irb(main):011:0> Micropost.last.created_at
  Micropost Load (1.0ms)  SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> Thu, 20 Apr 2023 09:52:23.940675000 UTC +00:00

②Micropost.firstを実行したときに発行されるSQL文はどうなっているでしょうか? 同様にして、Micropost.lastの場合はどうなっているでしょうか?(ヒント: それぞれをコンソール上で実行したときに表示される文字列が、SQL文になります。)
(解答)Micropost.firstでSELECT "microposts".* FROM "microposts" ORDER BY "microposts"."id" ASC LIMIT ?。Micropost.lastでSELECT "microposts".* FROM "microposts" ORDER BY "microposts"."id" DESC LIMIT ?。ASC LIMITとDESC LIMIT。

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

irb(main):002:0> user.microposts.first
  Micropost Load (2.5ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."id" ASC LIMIT ?  [["user_id", 1], ["LIMIT", 1]]  
=>                                                               
#<Micropost:0x00007f3886971830                                   
 id: 1,                                                          
 content: "Lorem ipsum",                                         
 user_id: 1,                                                     
 created_at: Thu, 20 Apr 2023 09:15:48.400796000 UTC +00:00,     
 updated_at: Thu, 20 Apr 2023 09:15:48.400796000 UTC +00:00> 

ユーザーを削除したらマイクロポストも削除された。

irb(main):005:0> Micropost.find(1)
  Micropost Load (0.3ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]                                               
/usr/local/bundle/ruby/3.1.0/gems/activerecord-7.0.4/lib/active_record/core.rb:284:in `find': Couldn't find Micropost with 'id'=1 (ActiveRecord::RecordNotFound)  

13.2.1

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

irb(main):001:0> helper.time_ago_in_words(3.weeks.ago)
=> "21 days"
irb(main):002:0> helper.time_ago_in_words(6.months.ago)
=> "6 months"

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

irb(main):003:0> helper.time_ago_in_words(1.year.ago)
=> "about 1 year"

③micropostsオブジェクトのクラスは何でしょうか? (ヒント: リスト 13.24内のコードを参考に、まずはpaginate(page: nil)でオブジェクトを取得し、その後classメソッドを呼び出してみましょう。)
(解答)

irb(main):006:0> microposts = user.microposts.paginate(page: nil)
  Micropost Load (2.5ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? LIMIT ? OFFSET ?  [["user_id", 100], ["LIMIT", 30], ["OFFSET", 0]]                            
=> []                                                                                  
irb(main):007:0> microposts.class
=> Micropost::ActiveRecord_AssociationRelation

13.2.2

①(1..10).to_a.take(6)というコードの実行結果を推測できますか? 推測した値が合っているかどうか、実際にコンソールを使って確認してみましょう。
(解答)1..10の配列に対して6番目まで取り出すから、1~6の配列が出てくる。

irb(main):001:0> (1..10).to_a.take(6)
=> [1, 2, 3, 4, 5, 6]

②上の演習にあったto_aメソッドの部分は本当に必要でしょうか? 確かめてみてください。
(解答) なくてもいける。takeは実行結果の配列を返すみたい。

irb(main):002:0> (1..10).take(6)
=> [1, 2, 3, 4, 5, 6]
irb(main):004:0> (1..10).take(6).class
=> Array
irb(main):005:0> (1..10).class
=> Range

③Fakerはlorem ipsum以外にも、非常に多種多様の事例に対応しています。Fakerのドキュメント(英語)を眺めながら画面に出力する方法を学び、実際に架空の大学名やHipster IpsumやChuck Norris facts(参考: チャック・ノリスの真実)を画面に出力してみましょう。
(解答) 飛ばします。

13.2.3

①リスト 13.29にある2つの'h1'のテストが正しいか確かめるため、該当するアプリケーション側のコードをコメントアウトしてみましょう。テストが green から red に変わることを確認してみてください。
(解答) コメントアウトするとテストRED。

      <h1>
        <%#= gravatar_for @user %>
        <%= @user.name %>
      </h1>

②リスト 13.29にあるテストを変更して、will_paginateが1回だけ表示されていることをテストしてみましょう。(ヒント: 表 5.2を参考にしてください。)
(解答) countを追加。

assert_select 'div.pagination',    count: 1

13.3.1

①なぜUsersコントローラ内にあるlogged_in_userフィルターを残したままにするとマズイのでしょうか? 考えてみてください。
(解答) applicationコントローラにもUsersコントローラにも配置するのはコードが重複しており、DRY原則に反するため。

13.3.2

①Homeページをリファクタリングして、if-else文の分岐のそれぞれに対してパーシャルを作ってみましょう。
(解答)
homeのビュー

[app/views/static_pages/home.html.erb]
<% if logged_in? %>
  <%= render 'static_pages/home_logged_in' %>
<% else %>
  <%= render 'static_pages/home_not_logged_in' %>
<% end %>

ログイン用パーシャル

[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>

非ログイン用パーシャル

[app/views/static_pages/_home_not_logged_in.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.svg", alt: "Rails logo", width: "200"),
                      "https://rubyonrails.org/" %>

②マイクロポストを投稿した直後に、ブラウザの更新ボタンを押すとエラーが表示されます。なぜエラーが表示されるのでしょうか?その原因を考えてみましょう。
(解答) 発生しなかったけど、POSTリクエストが重複して行われるから?

③もし上記の現象に対応するとしたら、どんな対応方法があるでしょうか?その対応方法を考えてみましょう。(ヒント: さまざまな対応方法がありますが、対応方法によっては今後の実装に支障が出ることがあります。ここでは対応方法のアイデア出しに留めておきましょう。)
(解答) 送信したら別のページへリダイレクト。ボタンの一時無効化など?

13.3.3

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

 Micropost Create (8.5ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "aaaaa"], ["user_id", 2], ["created_at", "2023-04-21 04:27:35.025993"], ["updated_at", "2023-04-21 04:27:35.025993"]]

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

irb(main):009:0> Micropost.where("user_id = ?", user.id) == user.microposts
  Micropost Load (0.3ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC  [["user_id", 2]]
  Micropost Load (0.5ms)  SELECT "microposts".* FROM "microposts" WHERE (user_id = 2) ORDER BY "microposts"."created_at" DESC
=> true                                                                                      
irb(main):010:0> Micropost.where("user_id = ?", user.id) == user.feed
=> true

13.3.4

①マイクロポストを作成し、その後、作成したマイクロポストを削除してみましょう。次に、Railsサーバーのログを開いて、DELETE文の内容を確認してみてください。
(解答)

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

②リスト 13.56の2つのリダイレクトを、redirect_back_or_to(root_url, status: :see_other)と1行で置き換えてもうまく動くことを、ブラウザを使って確認してみましょう。これは、参照元URLがnilである場合は指定のURLにリダイレクトします。
(解答)うまくいく。

13.3.5

①リスト 13.59で示した4つのテスト項目が正しく動いているかをテスト項目ごとに確認してみましょう。具体的には、対応するアプリケーション側のコードをコメントアウトし、テストが red になることを確認し、元に戻すと green になることを確認してみましょう。
(解答) テストは6つで全て正しく動作している。

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

class MicropostSidebarTest < MicropostsInterface

  test "should display the right micropost count" do
    get root_path
    assert_match "#{@user.microposts.count} microposts", response.body
  end

  test "should user proper pluralization for zero microposts" do
    log_in_as(users(:malory))
    get root_path
    assert_match "0 microposts", response.body
  end

  test "should user proper pluralization for one micropost" do
    log_in_as(users(:lana))
    get root_path
    assert_match "1 micropost", response.body
  end
end

13.4.1

①画像付きのマイクロポストを投稿してみましょう。画像が大きすぎますか?(次の13.4.3で画像サイズの問題を修正します)。
(解答) 飛ばします。

②リスト 13.67に示すテンプレートを参考に、13.4で実装した画像アップローダーをテストしてください。テストの準備として、まずはターミナルでリスト 13.66を入力し、サンプル画像をfixtureディレクトリに追加してください。継続を表すバックスラッシュ記号\は入力が必要ですが、シェルで自動的に追加される2行目冒頭の >記号は入力しないようご注意ください。リスト 13.67で追加したアサーションでは、Homeページにあるファイルアップロードと、投稿に成功した時に画像が表示されているかどうかをチェックしています。なお、テスト内にあるfixture_file_uploadというメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです22 。(ヒント: image属性が有効かどうかを確かめるときは、11.3.3で紹介したassignsメソッドを使ってください。このメソッドを使うと、投稿に成功した後にcreateアクション内のマイクロポストにアクセスするようになります。)
(解答)

[test/integration/microposts_interface_test.rb]
class ImageUploadTest < MicropostsInterface

  test "should have a file input field for images" do
    get root_path
    assert_select 'input[type=file]'
  end

  test "should be able to attach an image" do
    cont = "This micropost really ties the room together."
    img  = fixture_file_upload('kitten.jpg', 'image/jpeg')
    post microposts_path, params: { micropost: { content: cont, image: img } }
    assert assigns(:micropost).image.attached?
  end
end

13.4.2

①5MB以上の画像ファイルを送信しようとした場合、どうなりますか?
②無効な拡張子のファイルを送信しようとした場合、どうなりますか?
飛ばします。

13.4.3

①サイズの大きい画像をアップロードし、リサイズされているかどうか確認してみましょう。画像が長方形の場合も正しくリサイズされていますか?
飛ばします。

第13章ーまとめー

  • 外部キーでデータベースレベルでの制約を付けることによって、子テーブルが親テーブルの特定のデータを参照するようになる。
  • has_manyとbelongs_toで関連付けたモデルを形成できる。
  • デフォルトスコープ機能を使用することで、データの順序を指定可能になる。
  • object: f.objectのようにパーシャルの呼出しに対して変数を指定して渡すことが可能で、パーシャルの変数を動的に変えることが出来る。
  • SQL文に変数を代入する場合は、「?」でエスケープすることで、SQLインジェクションへの対策になる。
  • あるクラスに属する変数(@userのような)をrenderメソッドに渡すと、renderはその変数が属しているクラス(@userであればUserクラス)に該当するパーシャル(_user.html.erb)を持ってくる。更に@userがUserクラスのインスタンスであることを明示的に示している。
  • あるモデルに関連付けられたオブジェクトの作成、削除、取り出しなどでは、常に関連付けを通すことで第三者の削除を防止する等のセキュアな操作となる。
  • Active Strageを用いて画像のアップロードやバリデーションなどがおこなえる。

第13章終わり🐻