13.3 マイクロポストの操作
データモデリングとマイクロポスト表示テンプレートが完成したので、次はWeb経由でそれらを作成するためのインターフェイスに取りかかる。
従来のRails開発の慣習と異なる箇所
Micropostsリソースへのインターフェイスは、主に「プロフィールページ」と「Homeページ」のコントローラを経由して実行されるので、Micropostsのコントローラにはnew
やedit
のようなアクションは不要。つまり、create
とdestroy
があれば十分。
ルーティング
よって、Micropostsリソースは次のようになる。
Rails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' post '/signup', to: 'users#create' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users resources :account_activations, only: [:edit] resources :password_resets, only: [:new, :create, :edit, :update] resources :microposts, only: [:create, :destroy] #追加 end
13.3.1 マイクロポストのアクセス制御
Micropostsコントローラ内のアクセス制御から始める。
Micropostsは、関連付けられたユーザーを通してアクセスするので、create
アクションやdestroy
アクションを利用するユーザーは、ログイン済みでなければならない。
ログイン済みかどうかを確かめるテストでは、Usersコントローラ用のテストがそのまま役に立つ。つまり、
(1)正しいリクエストを各アクションに向けて発行
(2)マイクロポストの数が変化していないかどうか、
(3)リダイレクトされるかどうか
を確かめればよい。
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
②:login_url(ログイン画面)にリダイレクト
実装前のリファクタリング
上のテストにパスするコードを書くためには、少しアプリ側のコードをリファクタリングしておく。
というもの、beforeフィルターのlogged_in_user
メソッドを使って、ログインを要求したことについて思い出すとわかる。あのときはUsersコントローラ内にこのメソッドがあったので、beforeフィルターで指定していたが、このメソッドはMicropostsコントローラでも必要。
そこで、各コントローラが継承するApplicationコントローラに移動する。
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
logged_in_user
を削除する。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 # 正しいユーザーがどうかを確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end # 管理者かどうかを確認 def admin_user redirect_to(roo_url) unless current_user.admin? end end
Micropostsコントローラにbeforeフィルターを追加
Micropostsコントローラからもlogged_in_user
メソッドを呼び出せるようになった。
これにより、create
アクションやdestroy
アクションに対するアクセス制限が、beforeフィルターで簡単に実装できるようになった。
class MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] def create end def destroy end end
13.3.2 マイクロポストの作成
マイクロポストの作成フォームを、micropost/new
ページではなくホーム画面(つまりルートパス)にフォームに作る。これは、7章で、HTTP POSTリクエストをUsersコントローラのcreate
アクションに発行するHTMLフォームを作成することで、ユーザーのサインアップを実装したときと似ている。違うのは、ホーム画面にフォームを作る点。モックアップは次の図。
最後にホーム画面を実装したときは、[Sign up now!]ボタンが中央にあった。マイクロポスト作成フォームは、ログインしている特定のユーザーのコンテキストでのみ機能するので、ここでの1つの目標は、ユーザーのログイン状態に応じて、ホーム画面の表示を変更すること。
Micropostのcreate
アクション
マイクロポストのcreate
アクションもユーザー用アクションと似ている。違いは、新しいマイクロポストをbulid
するためにUser/Micropost関連付けを使っている点。
micropost_params
でStrong Parametersを使っていることにより、マイクロポストのcontent
属性だけがWeb経由で変更可能になっている点に注目!(下の①)
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
マイクロポスト作成フォーム
専用ページではなくstatic_pages/home.html.erb
にフォームを作る。
さらに、サイト訪問者がログインしているかどうかに応じて異なるHTMLを提供するコードを使う。
#ココから追加 <% 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> <% end %> #ココまで <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 "Sigh uo now!", signup_path, class: "btn btn-lg btn-primary" %> </div> <% end %>
このコードのif-else
文の分岐でコードを書きわけている点が少し汚いが、リファクタリングは演習。
いくつかのパーシャルを作る。まずはHomeページの新しいサイドバーから。
<%= 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>
pluralize
メソッドを使って調整している。次はマイクロポスト作成フォームを定義する。これはユーザー登録フォームに似ている。
<%= 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つ目の変更
homeアクションにマイクロポストのインスタンス変数@micropost
を追加。
class StaticPagesController < ApplicationController def home @micropost = current_user.microposts.build if logged_in? #追加 end :
current_user
メソッドはユーザーがログインしているときしか使えない。したがって、
@micropost
変数もログインしているとき定義されるようになる。2つ目の変更
エラーメッセージのパーシャルを再定義すること。
そうしないと_micropost_form.html.erb
の次のコードが動かない
<%= render 'shared/error_messages', object: f.object %>
なぜなら、今のエラーメッセージパーシャル(7.20)は↓で、fは@userのままだから)
<% 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 %>
@user
変数を直接参照していた。今回は
@microposts
変数を参照したい!@user
変数でも@microposts
変数でも参照できるようにしたい!そこで、フォーム変数
f
をf.object
とすることによって、関連付けられたオブジェクトにアクセスできるようにする。くり返すと、
form_for(@user) do |f|
上のように、f.object
が@user
となる場合と、
form_for(@micropost) do |f|
f.object
が@micropost
になる場合がある。
そこで、パーシャルにオブジェクトを渡すために、値がオブジェクトで、キーがパーシャルでの変数名と同じハッシュを利用する。これで、error_messages
パーシャルの2行目のコードが完成する。言い換えると、object: f.object
はerror_messages
パーシャルの中でobject
という変数名を作成してくれるので、この変数を使ってエラーメッセージを更新すればよいということ。
<% if object.errors.any? %> #@userをobjectに変更 <div id="error_explanation"> <div class="alert alert-danger"> The form contains <%= pluralize(object.errors.count, "error") %> #@userをobjectに変更 </div> <ul> <%= object.errors.full_messages.each do |msg| %> #@userをobjectに変更 <li><%= msg %></li> <% end %> </ul> </div> <% end %>
error_messages
パーシャルが、他のビュー(ユーザー登録、パスワード再設定、ユーザー編集)で使われているため。第二引数をobject: f.object
と更新するとテストは成功。
以下変更した結果を示す。
<% 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.text_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>
<% 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.text_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>
<% 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>
13.3.3 フィードの原型
Homeページにマイクロポストを表示する部分を実装する。
正しく動作しているかどうかを確認したい場合、正しいエントリーを投稿後、プロフィールページに移動してポストを確認すればよい。が、これはかなり面倒な作業。ユーザー自身のポストを含むマイクロポストのフィードがないと不便。
すべてのユーザーがフィードを持つので、feed
メソッドはUserモデルに作る。
フィードの原型では、現在ログインしているユーザーのマイクロポストをすべて取得する。
なお、14章で完全なフィードを実装するため、where
メソッドで実装↓
: #試作feedの定義 #完全な実装は14章を参照 def feed Micropost.where("user_id = ?", id) end private
Micropost.where("user_id = ?", id)について
プレースホルダー(?のこと)を使った書き方。?に第二引数id
が置き換わる。
これにより、SQLインジェクションと呼ばれるセキュリティホールを避けることができる。
feed
アクションでやってることは、MIcropostテーブルからuser_id
がid
のユーザーをすべて取得している(14章で完璧に実装する)。
また、上のコードは本質的には次のコードと同等
def feed microposts end
このコードを使わなかったのは、14章での応用のため。
フィード機能の実装
次の流れで実装する。
(1)現在のユーザーのページ分割されたフィードに@feed_items
インスタンス変数を追加し、
(2)次に、フィード用のパーシャルをHomeページに追加
Homeページに変更を加えるとき、ユーザーがログインしているかどうかを調べる後置if文が変化している点に注目!
@microopost = current_user.microposts.build if logged_in?
上のコードが、次のような前置if文に変わっている。
if logged_in? @micropost = current_user.microposts.bulid @feed_items = current_user.feed.paginate(page: params[:page]) end
(1行のときは後置if文、2行以上の時は前置if文を使うのがRubyの慣習)
def home if logged_in? @micropost = current_user.microposts.build @feed_items = current_user.feed.paginate(page: params[:page]) end end :
<% if @feed_items.any? %> <ol class="microposts"> <%= render @feed_items %> </ol> <%= will_paginate @feed_items %> <% end %>
<%= render @feed_items %>
このとき、@feed_items
の各要素がMicropost
クラスを持っていたため、RailsはMicropostのパーシャルを呼び出すことができる。このように、Railsは対応する名前のパーシャルを、渡されたリソースのディレクトリ内から探しにいくことができる。
app/views/microposts/_micropost.html.erb
あとは、いつものようにフィードパーシャルを表示すればHomeページにフィードを追加できる。
<% 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
インスタンス変数を期待しているため、現状では壊れてしまう。最も簡単な解決方法は、@feed_items
に空の配列を渡しておくこと。
: 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 : private def micropost_params params.require(:micropost).permit(:content) end end
13.3.4 マイクロポストの削除
最後の機能として、マイクロポストリソースにポストを削除する機能を追加する。これはユーザー削除と同様に"delete"リンクで実現する。ユーザーの削除は管理者ユーザーのみが行えるよう制限されていたのに対し、今回は自分が投稿したマイクロポストに対してのみ削除リンクが動作するようにする。
最初のステップとして、マイクロポストのパーシャルに削除リンクを追加する。
<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. <% if current_user?(micropost.user) %> <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %> #追加 <% end %> </span> </li>
次に、Micropostsコントローラのdestroy
アクションを定義する。
これも、ユーザーにおける実装とだいたい同じ。大きな違いは、admin_user
フィルターで@user
変数を使うのではなく、関連付けを使ってマイクロポストを見つけようとしている点。これにより、あるユーザーが他のユーザーのマイクロポストを削除しようとすると、自動的に失敗するようになる。
具体的には、correct_user
フィルター内でfind
メソッドを呼び出すことで、現在のユーザーが削除対象のマイクロポストを保有しているかどうか確認する。
class MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] before_action :correct_user, only: :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 @micropost.destroy #追加 flash[:success] = "Micropost deleted" redirect_to request.referrer || root_url #① end private def micropost_params params.require(:micropost).permit(:content) end def correct_user @micropost = current_user.microposts.find_by(id: params[:id]) #追加 redirect_to root_url if @micropost.nil? end end
destroy
メソッドでリダイレクトを使っている点に注目。ここではrequest.referrer
というメソッドを使っている。このメソッドはフレンドリーフォワーディングのrequest.url
変数と似ていて、1つ前のURLを返す。(今回の場合はHomeページになる)このため、マイクロポストがHomeページから削除された場合でもプロフィールページから削除された場合でも、
request.referrer
を使うことでDELETEリクエストが発行されたページに戻すことができるので、非常に便利!!ちなみに、元に戻すURLが見つからなかった場合でも(例えばテストでは
nil
が返ってくる場合がある)、||演算子でroot_url
をデフォルトで設定しているため、エラーが出ずに/へと飛ぶ。これらのコードにより、上から2番目のマイクロポストを削除してみると、うまく動くはず。