日別アーカイブ: 2021年12月5日

Rails-tutorial(13.3マイクロポストを操作する 主に演習)

その13.2から続く

13.3 マイクロポストを操作する

次はWeb経由でそれらを作成するためのインターフェイスに
取りかかりましょう。この節では、ステータスフィード
(第14章で完成させます) の最初のヒントをお見せします。
最後に、ユーザーがマイクロポストをWeb経由で破棄できるようにします。

Micropostsリソースへのインターフェイスは、主にプロフィールページとHomeページのコントローラを経由して実行されるので、
Micropostsコントローラにはnewやeditのようなアクションは不要と
いうことになります。つまり、createとdestroyがあれば十分です。

マイクロポストリソースのルーティング
config/routes.rb

#追加
resources :microposts, only: [:create, :destroy]
HTTPリクエスト URL    アクション 名前付きルート
POST        /microposts create microposts_path
DELETE       /microposts/1 destroy micropost_path(micropost)

13.3.1 マイクロポストのアクセス制御

Micropostsリソースの開発では、Micropostsコントローラ内の
アクセス制御から始めることにしましょう。
関連付けられたユーザーを通してマイクロポストにアクセスするので、createアクションやdestroyアクションを利用するユーザーは、ログイン済みでなければなりません。

ログイン済みかどうかを確かめるテストでは、
Usersコントローラ用のテストがそのまま役に立ちます。

正しいリクエストを各アクションに向けて発行し、
マイクロポストの数が変化していないかどうか、
また、リダイレクトされるかどうかを確かめればよいのです。

Micropostsコントローラの認可テスト red
test/controllers/microposts_controller_test.rb

require 'test_helper'

class MicropostsControllerTest < ActionDispatch::IntegrationTest

def setup
  @micropost = microposts(:orange)
end

test "should redirect create when not logged in" do
 assert_no_difference 'Micropost.count' do
  post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
 end
 assert_redirected_to login_url
end

test "should redirect destroy when not logged in" do
 assert_no_difference 'Micropost.count' do
  delete micropost_path(@micropost)
 end
 assert_redirected_to login_url
 end
end

少しアプリケーション側のコードをリファクタリングしておく必要があります。
というのも、beforeフィルターのlogged_in_userメソッドを使って、
ログインを要求したことについて思い出してください。

Usersコントローラ内にこのメソッドがあったので、
beforeフィルターで指定していましたが、
このメソッドはMicropostsコントローラでも必要です。
そこで、各コントローラが継承するApplicationコントローラに
(4.4.4)、このメソッドを移してしまいましょう。

ogged_in_userメソッドをApplicationコントローラに移す
app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper

private

# ユーザーのログインを確認する
def logged_in_user
 unless logged_in?
  store_location
  flash[:danger] = "Please log in."
  redirect_to login_url
  end
 end
end

コードが重複しないよう、このときUsersコントローラからも
logged_in_userを削除しておきましょう

Usersコントローラ内のlogged_in_userフィルターを削除する red
app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeフィルター

    # 正しいユーザーかどうかを確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end

    # 管理者かどうかを確認
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

Micropostsコントローラからもlogged_in_userメソッドを呼び出せる
ようになりました。これにより、createアクションやdestroyアクションに
対するアクセス制限が、beforeフィルターで簡単に実装できるようになります

Micropostsコントローラの各アクションに認可を追加する green
app/controllers/microposts_controller.rb

class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]

 def create
 end

 def destroy
 end
end

演習

1:なぜUsersコントローラ内にあるlogged_in_userフィルターを
残したままにするとマズイのでしょうか? 考えてみてください。

コードが重複して思わぬ動作が起きるから

13.3.2 マイクロポストを作成する

最後にホーム画面を実装したときは[Sign up now!]ボタンが中央にありましたマイクロポスト作成フォームは、
ログインしている特定のユーザーのコンテキストでのみ機能するので、
この節の一つの目標は、
ユーザーのログイン状態に応じて、ホーム画面の表示を変更することです。

次に、マイクロポストのcreateアクションを作り始めましょう。
このアクションも、ユーザー用アクションと似ています。違いは、
新しいマイクロポストをbuildするためにUser/Micropost関連付けを
使っている点です

micropost_paramsでStrong Parametersを使っていることにより、
マイクロポストのcontent属性だけがWeb経由で変更可能になっている点に
注目してください。

Micropostsコントローラのcreateアクション
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'
 end
end

def destroy
end

private

  def micropost_params
    params.require(:micropost).permit(:content)
  end
end

マイクロポスト作成フォームを構築するために、
サイト訪問者がログインしているかどうかに応じて異なるHTMLを提供するコードを使います

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

コードを動かすためには、いくつかのパーシャルを作る必要があります。まずはHomeページの新しいサイドバーからです。
次のようになります。

サイドバーで表示するユーザー情報のパーシャル
app/views/shared/_user_info.html.erb

<%= link_to gravatar_for(current_user, size: 50),
current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %></span>
<span><%= pluralize(current_user.microposts.count,
"micropost") %></span>

そのユーザーが投稿したマイクロポストの総数が表示されていることに注目してください。ただし少し表示に違いがあります。

プロフィールサイドバーでは、 “Microposts” をラベルとし、
「Microposts (1)」と表示することは問題ありません。
しかし、今回のように “1 microposts” と表示してしまうと
英語の文法上誤りになってしまいます。そこで、pluralizeメソッドを使って
“1 micropost” や “2 microposts” と表示するように調整しています。

 

次はマイクロポスト作成フォームを定義します。
これはユーザー登録フォームに似ています。
マイクロポスト投稿フォームのパーシャル

app/views/shared/_micropost_form.html.erb

<%= form_for(@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 %>

フォームが動くようにするためには、2箇所の変更が必要です。1つは、
(以前と同様) 関連付けを使って次のように@micropostを定義することです。

homeアクションにマイクロポストのインスタンス変数を追加するapp/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  def home
    @micropost = current_user.microposts.build if logged_in?
  end

  def help
  end

  def about
  end

  def contact
  end
end

current_userメソッドはユーザーがログインしているときしか使えません
つまり、@micropost変数もログインしているときのみ
定義されるようになります。

リスト13.39を動かすためのもう1つの変更は、
エラーメッセージのパーシャルを再定義することです。でなければ、
次のコードが動きません。

<%= render 'shared/error_messages', object: f.object %>

form_for(@user) do |f|上のようにf.objectが@userとなる場合と、

form_for(@micropost) do |f|

上のようにf.objectが@micropostになる場合などがあります。

パーシャルにオブジェクトを渡すために、値がオブジェクトで、
キーがパーシャルでの変数名と同じハッシュを利用します。
これで、2行目のコードが完成します。言い換えると、object: f.objectはerror_messagesパーシャルの中でobjectという変数名を
作成してくれるので、この変数を使ってエラーメッセージを更新すればよいということです。

Userオブジェクト以外でも動作するようにerror_messagesパーシャルを更新する red
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 %>

なぜ失敗しているのでしょうか。error_messagesパーシャルの他の
出現場所です。
このパーシャルは他の場所でも使われていたため、
ユーザー登録、パスワード再設定、そしてユーザー編集のそれぞれのビューを更新する必要があったのです。
各ビューを更新した結果を示します。

ユーザー登録時のエラー表示を更新するapp/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>
      <%= 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 "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
リスト 13.44: ユーザー編集時のエラー表示を更新するapp/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>

      <%= 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 "Save changes", class: "btn btn-primary" %>
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails">change</a>
    </div>
  </div>
</div>
リスト 13.45: パスワード再設定時のエラー表示を更新するapp/views/password_resets/edit.html.erb
<% provide(:title, 'Reset password') %>
<h1>Password reset</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>

      <%= hidden_field_tag :email, @user.email %>

      <%= 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 "Update password", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

これで、すべてのテストが greenになるはずです。

rails test

演習

1:Homeページをリファクタリングして、if-else文の分岐のそれぞれに対してパーシャルを作ってみましょう。

<% if logged_in? %>
<%= render 'static_pages/if'%>
<% else %>
<%= render 'static_pages/else'%>
<% end %>

13.3.3 フィードの原型

マイクロポスト投稿フォームが動くようになりましたが、今の段階では投稿した内容をすぐに見ることができません。Homeページにまだマイクロポストを表示する部分が実装されていないからです。

すべてのユーザーがフィードを持つので、feedメソッドはUserモデルで作るのが自然です。
フィードの原型では、まずは現在ログインしているユーザーの
マイクロポストをすべて取得してきます。
なお、次章で完全なフィードを実装するため、
今回は紹介したwhereメソッドでこれを実現します。
Userモデルに変更を加えた結果を示します

マイクロポストのステータスフィードを実装するための準備
app/models/user.rb

# 試作feedの定義
# 完全な実装は次章の「ユーザーをフォローする」を参照
def feed
  Micropost.where("user_id = ?", id)
end

疑問符は、セキュリティ上重要な役割を↑果たしています。

SQLクエリに代入する前にidがエスケープされるため、SQLインジェクション(SQL Injection)と呼ばれる深刻なセキュリティホールを避けることができます。
この場合のid属性は単なる整数 (すなわちself.idはユーザーのid)
であるため危険はありませんが、
SQL文に変数を代入する場合は常にエスケープする習慣をぜひ身につけてください。

↑のコードは本質的に↓のコードと同等であることに気付くかもしれません。

def feed
  microposts
end

サンプルアプリケーションにフィード機能を導入するため、
ログインユーザーのフィード用にインスタンス変数@feed_itemsを追加し、Homeページにはフィード用のパーシャルを追加します。

homeアクションにフィードのインスタンス変数を追加する
app/controllers/static_pages_controller.rb

class StaticPagesController < ApplicationController

def home
  if logged_in?
    @micropost = current_user.microposts.build
    @feed_items = current_user.feed.paginate(page: params[:page])
  end
end

ステータスフィードのパーシャル
app/views/shared/_feed.html.erb

<% if @feed_items.any? %>
 <ol class="microposts">
  <%= render @feed_items %>
 </ol>
 <%= will_paginate @feed_items %>
<% end %>

このとき、@feed_itemsの各要素がMicropostクラスを持っていたため、RailsはMicropostのパーシャルを呼び出すことができました。
このように、Railsは対応する名前のパーシャルを、
渡されたリソースのディレクトリ内から探しにいくことができます。

app/views/microposts/_micropost.html.erb

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 class="col-md-8">
 <h3>Micropost Feed</h3>
  <%= render 'shared/feed' %>
 </div>
</div>
<% else %>
#後略
<% end %>

マイクロポストの投稿が失敗すると、Homeページは
@feed_itemsインスタンス変数を期待しているため、現状では壊れてしまいます。

最も簡単な解決方法は、空の配列を渡しておくことです。
残念ですが、この場合はページ分割されたフィードを返してもうまく動きません。

createアクションに空の@feed_itemsインスタンス変数を追加する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
      @feed_items = []
      render 'static_pages/home'
    end
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end

演習

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

INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "tet\r\n"], ["user_id", 1], ["created_at", "2020-04-10 09:48:54.428673"], ["updated_at", "2020-04-10 09:48:54.428673"]]

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

user = User.first
Micropost.where("user_id = ?", user.id) == user.microposts
Micropost Load (1.1ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC [["user_id", 1]]
Micropost Load (0.3ms) SELECT "microposts".* FROM "microposts" WHERE (user_id = 1) ORDER BY "microposts"."created_at" DESC
=> true
>> user.feed == user.microposts

13.4に続く