ぞえの技術めも

Ruby on Rails勉強中

【156日目】【1日20分のRailsチュートリアル】【第11章】マイクロポストを削除する

Ruby on Railsチュートリアル(第3版)

今日は「11.3.4 マイクロポストを削除する」から。

11.3.4 マイクロポストを削除する

最後の機能として、マイクロポストリソースにポストを削除する機能を追加します。これはユーザー削除と同様に(9.4.2)、"delete" リンクで実現します (図11.16)。
(中略)
カレントユーザーが作成したマイクロポストに対してのみ削除リンクが動作するようにします。

自分が投稿したマイクロポストしか消せないようにしないとね。

最初のステップとして、マイクロポストのパーシャル (リスト11.21) に削除リンクを追加します。

マイクロポストの投稿ユーザーとカレントユーザーが一致するときは削除リンクを表示する。

app/views/microposts/_micropost.html.erb

    :
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>
    :

次に、Micropostsコントローラのdestroyアクションを定義しましょう。
(中略)
大きな違いは、admin_userフィルターで@user変数を使うのではなく、関連付けを使ってマイクロポストを見つけるようにしている点です。

マイクロポストのidは一意なので、カレントユーザーのマイクロポストからfindしたときに見つからない→カレントユーザーのマイクロポストじゃない→削除できない、ってできるってことかな。
削除リンクも表示させないけど、destroyアクション呼ばれたときにもチェックしてあるユーザーが他のユーザーのマイクロポストを削除しようとするのを防ぐのかー。

app/controllers/microposts_controller.rb

  :
  before_action :correct_user,   only: :destroy
  :
  def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    redirect_to request.referrer || root_url
  end

  private
    :
    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
end

マイクロポストがHomeページから削除された場合でもProfileページから削除された場合でも、request.referrerを使うことでDELETEリクエストが発行されたページに戻すことができるので、非常に便利です。

URLって保存されてるんだなぁ…。

これらのコードにより、上から2番目のマイクロポストを削除すると、図 11.17のようにうまく動くはずです。

動作見てみよう。

サーバーを起動して

$ rails server -b $IP -p $PORT

上から2番目のマイクロポストを削除してみる。

確認のポップアップも表示されて

f:id:kt_zoe:20170626123549p:plain

マイクロポストも削除できました。

f:id:kt_zoe:20170626123606p:plain

今日の学習時間は【24分】

次は「11.3.5 フィード画面におけるマイクロポストのテスト」から。

【155日目】【1日20分のRailsチュートリアル】【第11章】お試しフィードを追加する

Ruby on Railsチュートリアル(第3版)

今日は「11.3.3 フィードの原型」から。

11.3.3 フィードの原型

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

ホーム画面でマイクロポスト投稿してからプロフィール画面に行けば見れるけど、めんどいよね!ってことか。そうだね、微妙に手間だね。

すべてのユーザーがフィードを持つので、feedメソッドはUserモデルで作るのが自然です。フィードの原型では、まずは現在ログインしているユーザーのマイクロポストをすべて取得してきます。(リスト11.44)

whereメソッドを使って現在ログインしているユーザーのマイクロポストを全て取得。

app/models/user.rb

  :
  # 試作feedの定義
  # 完全な実装は第12章「ユーザーをフォローする」を参照してください。
  def feed
    Micropost.where("user_id = ?", id)
  end
  :

上の疑問符があることで、SQLクエリにインクルードされる前にidが適切にエスケープされることを保証してくれるため、SQLインジェクションと呼ばれる深刻なセキュリティホールを避けることができます。

へー。SQLクエリ使うときは気をつけなきゃいけないのか。。。

サンプルアプリケーションでフィードを使うために、カレントユーザーのページ分割されたフィードに@feed_itemsインスタンス変数を追加し (リスト11.45)、次にフィード用のパーシャル (リスト11.46) をHomeページに追加します。

homeアクションに@feed_itemsインスタンス変数を追加。

app/controllers/static_pages_controller.rb

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

フィード用のパーシャルはまずファイルを生成して

$ touch app/views/shared/_feed.html.erb

下記内容で更新。

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のパーシャルの呼び出すことができました。

Micropostクラスを持っていたら@feed_itemsで「1つのマイクロポストを表示するパーシャル」を呼び出すことができるんだ…!なんか不思議。

後は、いつものようにフィードパーシャルを表示すればHomeページにフィードを追加できます (リスト11.47)。

マイクロポストの投稿フォームタグの下にフィードを追加。

app/views/static_pages/home.html.erb

<% if logged_in? %>
    :
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
    :

動作確認のためにサーバーを起動して

$ rails server -b $IP -p $PORT

ホーム画面にフィードが表示されました。

f:id:kt_zoe:20170623124901p:plain

マイクロポスト投稿もOK!

f:id:kt_zoe:20170623124925p:plain

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

試しに空で投稿してみたらエラー画面が。。。

f:id:kt_zoe:20170623124950p:plain

最も簡単な解決方法は、リスト11.48のように空の配列を渡しておくことです。

とりあえずcreateアクションで空の配列渡すようにしておきましょう。

app/controllers/microposts_controller.rb

    :
    else
      @feed_items = []
      render 'static_pages/home'
    end
    :

これするとどうなるのかなー、と思ったらフィードが表示されなくなるのか。。。まぁ空の配列渡してるもんね。

f:id:kt_zoe:20170623125008p:plain

今日の学習時間は【28分】

次は「11.3.4 マイクロポストを削除する」から。

【154日目】【1日20分のRailsチュートリアル】【第11章】マイクロポスト作成フォームを定義する

Ruby on Railsチュートリアル(第3版)

今日は「11.3.2 マイクロポストを作成する」のマイクロポスト作成フォームを定義するところから。

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

次はマイクロポスト作成フォームを定義します (リスト11.37)。

これもファイルから作成します。

$ touch app/views/shared/_micropost_form.html.erb

できたファイルを下記内容で更新。マイクロポストを入力してPOSTで送信するフォームを定義。

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

1つは、(以前と同様) 関連付けを使用して次のように@micropostを定義することです。

homeアクションにマイクロポストのインスタンス変数を追加。

app/controllers/static_pages_controller.rb

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

リスト11.37を動かすためのもう1つの変更は、エラーメッセージのパーシャルを再定義することです。
(中略)
object: f.objectはerror_messagesパーシャルの中でobjectという変数名を作成してくれるので、この変数を使ってエラーメッセージを更新すればよいということです (リスト11.39)。

うーん、なんか難しい…。
error_messagesパーシャルを汎用的に使えるようにするためのあれこれってことだよね…。

@userとしてたところを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 %>

この時点でテストを走らせてみてください。テストが失敗したままになっています。

$ bundle exec rake test
ERROR["test_password_resets", PasswordResetsTest, 2017-06-08 11:12:25 +0000]
  :
57 tests, 286 assertions, 0 failures, 1 errors, 0 skips

失敗しますね。

このパーシャルは他の場所でも使われていたため、ユーザー登録 (リスト7.18)、パスワード再設定 (リスト10.50)、そしてユーザー編集 (リスト9.2) のそれぞれのビューを更新する必要があったのです。

パスワード再設定のやつしかログには出てないけど、いいのかな。

app/views/password_resets/edit.html.erb

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

試しにパスワード再設定だけ直してテスト実施してみる。

$ bundle exec rake test
57 tests, 294 assertions, 0 failures, 0 errors, 0 skips

…通っちゃったよ!!何故だ……

ユーザー登録のソースコード見てみると、「9.6 演習」のnewフォームとeditフォームをパーシャル化するの対応がmasterに入っちゃってるっぽい。
あれー、演習の内容マージしたっけな…。エラー出てないしこっちは直さなくてもいいのかな。

パーシャルの方でobject: @userって指定してるから問題ないのかも。でも一応f.objectに統一しておくか。

app/views/users/_form.html.erb

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

修正してもテストが通ることを確認。

$ bundle exec rake test
57 tests, 294 assertions, 0 failures, 0 errors, 0 skips

さらに、この章で作成したすべてのHTMLが適切に表示されるようになったはずです。

動作確認してみましょう。

サーバーを起動して

$ rails server -b $IP -p $PORT

ホーム画面にアクセスしてみる。

おぉ、マイクロポスト投稿フォームができてる!

f:id:kt_zoe:20170621123854p:plain

空のまま「Post」をクリックしてエラーメッセージが出ることも確認。

f:id:kt_zoe:20170621123911p:plain

今日の学習時間は【35分】

次は「11.3.3 フィードの原型」から。

【153日目】【1日20分のRailsチュートリアル】【第11章】マイクロポスト作成フォームを構築するためにホーム画面を変更する

Ruby on Railsチュートリアル(第3版)

今日は「11.3.2 マイクロポストを作成する」から。

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

主な違いは、別の micropost/new ページを使う代わりに、ホーム画面 (つまりルートパス) にフォームを置くという点です。図11.10モックアップを見てください。

ユーザーの新規登録と似てるけど、新規ページを作るんじゃなくて、ホーム画面にフォームを追加するのかー。

マイクロポストのcreateアクションを作り始めましょう。
(中略)
違いは、新しいマイクロポストをbuildするためにuser/micropost関連付けを使用している点です (リスト11.34)。

user/micropost関連付けを使用することでどのユーザーのマイクロポストなのか紐付けることができる、って感じなのかな。

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
      render 'static_pages/home'
    end
  end
  :
  private

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

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

ログイン中かどうかをチェックして、ログインしていたらユーザー情報とマイクロポストのフォームを表示して、ログインしてなかったら今までのホーム画面を表示するようにビューを変更。

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

if-else分岐を使用してコードを書き分けている点が少し汚いですが、このコードのクリーンアップは演習に回すことにします (11.6)。

え、これ駄目なんだ……。まぁまた演習で。

リスト11.35のコードを動かすためには、いくつかのPartialを作る必要があります。まずはHomeページの新しいサイドバーからです。以下のリスト11.36のようになります。

Partial作成用にファイルを作成する。

$ touch app/views/shared/_user_info.html.erb

下記のように更新。
ユーザーのプロフィールアイコン画像と名前、プロフィールページへのリンク、マイクロポストの投稿数を表示するコードを生成するPartialを追加する。

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>

今回のように “1 microposts” と表示してしまうと英語の文法上誤りになってしまいます。そこで、7.3.3で紹介したpluralizeメソッドを使って “1 micropost” や “2 microposts” と表示するように調整しています。

そういえばそんなメソッドありましたね。細かい…!!日本語で表示するならこの辺は気にしなくて良さそう。

項の途中だけどキリがいいので今日はここまで。

今日の学習時間は【21分】

次は「11.3.2 マイクロポストを作成する」のマイクロポスト作成フォームを定義するところから。

【152日目】【1日20分のRailsチュートリアル】【第11章】マイクロポストのアクセス制御を実装する

Ruby on Railsチュートリアル(第3版)

今日は「11.3 マイクロポストを操作する」から。

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

データモデリングとマイクロポスト表示テンプレートの両方が完成したので、次はWeb経由でそれらを作成するためのインターフェイスに取りかかりましょう。

Twitterでいう新規ツイートするフォームかな。

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

newも要らないのか。Micropostsを投稿する専用ページは設けないイメージかな…??

ルーティングファイルに下記を追加。

config/routes.rb

  resources :microposts,          only: [:create, :destroy]

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

関連付けられたユーザーを通してマイクロポストにアクセスするので、createアクションやdestroyアクションを利用するユーザーは、ログイン済みでなければなりません。

ふむふむ。確かにそうですね。
誰でも利用できる状態はまずいですね。

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

createとdestroyアクションに対してテストを追加。

test/controllers/microposts_controller_test.rb

require 'test_helper'

class MicropostsControllerTest < ActionController::TestCase

  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post :create, 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 :destroy, id: @micropost
    end
    assert_redirected_to login_url
  end
end

実装してないのでテストは通りません。

$ bundle exec rake test
57 tests, 290 assertions, 0 failures, 2 errors, 0 skips

beforeフィルターのlogged_in_userメソッドを使って、ログインを要求したことについて思い出してください (リスト9.12)。
(中略)
そこで、各コントローラが継承するApplicationコントローラに (4.4.4)、このメソッドを移してしまいましょう。結果はリスト11.31のようになります。

各コントローラで使用するAPIは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を削除しておきましょう。

コメントアウトしておきました。

app/controllers/users_controller.rb

    # # ログイン済みユーザーかどうか確認
    # def logged_in_user
    #   unless logged_in?
    #     store_location
    #     flash[:danger] = "Please log in."
    #     redirect_to login_url
    #   end
    # end

これにより、createアクションやdestroyアクションに対するアクセス制限が、beforeフィルターで簡単に実装できるようになります (リスト11.32)。

Userコントローラと一緒の実装でOKなのかー。beforeフィルターとcreate/destroyアクションを追加。

app/controllers/microposts_controller.rb

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

  def create
  end

  def destroy
  end
end

テスト通りました!

$ bundle exec rake test
57 tests, 294 assertions, 0 failures, 0 errors, 0 skips

今日の学習時間は【30分】

次は「11.3.2 マイクロポストを作成する」から。

【151日目】【1日20分のRailsチュートリアル】【第11章】プロフィール画面におけるマイクロポストのテストを追加する

Ruby on Railsチュートリアル(第3版)

今日は「11.2.3 プロフィール画面におけるマイクロポストのテスト」から。

11.2.3 プロフィール画面におけるマイクロポストのテスト

この項では、プロフィール画面で表示されるマイクロポストに対して、統合テストを書いていきます。まずは、プロフィール画面用の統合テストを生成してみましょう。

この段階でプロフィール画面用の統合テストを生成するのか。。。もっと早いのもありなのかな。

$ rails generate integration_test users_profile
      invoke  test_unit
      create    test/integration/users_profile_test.rb

プロフィール画面におけるマイクロポストをテストするためには、ユーザーに紐付いたマイクロポストのテスト用データが必要になります。

まずはテスト用データを準備する。

元々追加していたマイクロポスト用のテスト用データをmichaelに紐付けるのと追加で30個用意する。

test/fixtures/microposts.yml

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael
  
tau_manifesto:
  content: "Check out the @tauday site by @mhartl: http://tauday.com"
  created_at: <%= 3.years.ago %>
  user: michael

cat_video:
  content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
  created_at: <%= 2.hours.ago %>
  user: michael

most_recent:
  content: "Writing a short test"
  created_at: <%= Time.zone.now %>
  user: michael

<% 30.times do |n| %>
micropost_<%= n %>:
  content: <%= Faker::Lorem.sentence(5) %>
  created_at: <%= 42.days.ago %>
  user: michael
<% end %>

今回のテストでは、プロフィール画面にアクセスした後に、ページタイトルとユーザー名、Gravatar、マイクロポストの投稿数、そしてページ分割されたマイクロポスト、といった順でテストしていきます。

プロフィール画面の表示を順にテストしていく感じかな。

test/integration/users_profile_test.rb

require 'test_helper'

class UsersProfileTest < ActionDispatch::IntegrationTest
  include ApplicationHelper

  def setup
    @user = users(:michael)
  end

  test "profile display" do
    get user_path(@user)
    assert_template 'users/show'
    assert_select 'title', full_title(@user.name)
    assert_select 'h1', text: @user.name
    assert_select 'h1>img.gravatar'
    assert_match @user.microposts.count.to_s, response.body
    assert_select 'div.pagination'
    @user.microposts.paginate(page: 1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end
end

したがって、そのページのどこかしらにマイクロポストの投稿数が存在するのであれば、次のように探し出してマッチできるはずです。
assert_match @user.microposts.count.to_s, response.body

ページにマイクロポストの投稿数なんて表示してたっけ。。。って思ったら表示してました。
キャプチャ見れば一目瞭然。(「Microposts」の末尾にあった)

f:id:kt_zoe:20170612123515p:plain

そして、アプリケーション側のコードは実装済みなので、これらのテストは成功するはずです。

成功しました。問題なし!

$ bundle exec rake test
55 tests, 290 assertions, 0 failures, 0 errors, 0 skips

今日の学習時間は【20分】

次は「11.3 マイクロポストを操作する」から。

【150日目】【1日20分のRailsチュートリアル】【第11章】マイクロポストのサンプルデータを追加する

Ruby on Railsチュートリアル(第3版)

今日は「11.2.2 マイクロポストのサンプル」から。

11.2.2 マイクロポストのサンプル

すべてのユーザーにマイクロポストを追加しようとすると時間が掛かり過ぎるので、takeメソッドを使って最初の6人だけに追加します。
(中略)
この6人については、1ページの表示限界数 (30) を越えさせるために、それぞれ50個分のマイクロポストを追加するようにしています。

ほほぅ…。適当な任意のユーザー1人だけ、とかじゃないのか。
そっか、自動生成するんだもんね。。。何人でも何個でもできるか。

“テストデータ"って考えると手作業で適当なデータ入れる手順を思い浮かべてしまうんだけど、自動生成できるんだもんねぇ。 そう、Ruby on Railsならね。(

というわけでサンプルデータにマイクロポスト追加しましょう。

db/seeds.rb

users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(5)
  users.each { |user| user.microposts.create!(content: content) }
end

ここで、いつものように開発環境用のデータベースで再度サンプルデータを生成します。

$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed

よし、サンプルデータを再生成したので実際の表示を見てみよう。

サーバーを起動して

$ rails server -b $IP -p $PORT

example@railstutorial.orgのアカウントでログイン。

プロフィールページにマイクロポストが表示されました!

f:id:kt_zoe:20170612123428p:plain

図11.6のページにはマイクロポスト固有のスタイルが与えられていないので、リスト11.25を追加して、結果のページを見てみましょう。

レイアウトひどいなと思ってました。スタイル追加しましょう。

app/assets/stylesheets/custom.css.scss

  :
/* microposts */

.microposts {
  list-style: none;
  padding: 0;
  li {
    padding: 10px 0;
    border-top: 1px solid #e8e8e8;
  }
  .user {
    margin-top: 5em;
    padding-top: 0;
  }
  .content {
    display: block;
    margin-left: 60px;
    img {
      display: block;
      padding: 5px 0;
    }
  }
  .timestamp {
    color: $gray-light;
    display: block;
    margin-left: 60px;
  }
  .gravatar {
    float: left;
    margin-right: 10px;
    margin-top: 5px;
  }
}

aside {
  textarea {
    height: 100px;
    margin-bottom: 5px;
  }
}

span.picture {
  margin-top: 10px;
  input {
    border: 0;
  }
}

きれいになったー!

f:id:kt_zoe:20170612123515p:plain

他の任意のユーザーもマイクロポスト表示されてます。

f:id:kt_zoe:20170612123535p:plain

ページャーも問題なし。

f:id:kt_zoe:20170612123558p:plain

今日の学習時間は【25分】

次は「11.2.3 プロフィール画面におけるマイクロポストのテスト」から。