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