ぞえの技術めも

Ruby on Rails勉強中

【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 プロフィール画面におけるマイクロポストのテスト」から。

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

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

今日は「11.2.1 マイクロポストの描画」から。

11.2.1 マイクロポストの描画

この項では、ユーザーのプロフィール画面 (show.html.erb) でそのユーザーのマイクロポストを表示させ、また、これまでに投稿した総数も表示するようにしていきます。

ふむふむ。Twitterのプロフィール画面のイメージかな。

まずは、Micropostのコントローラとビューを作成するために、コントローラを生成しましょう。

生成しましょう。

$ rails generate controller Microposts
      create  app/controllers/microposts_controller.rb
      invoke  erb
      create    app/views/microposts
      invoke  test_unit
      create    test/controllers/microposts_controller_test.rb
      invoke  helper
      create    app/helpers/microposts_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/microposts.coffee
      invoke    scss
      create      app/assets/stylesheets/microposts.scss

user.html.erbパーシャルと同じようにmicropost.html.erbパーシャルを定義してマイクロポストを表示するようにする。

パーシャル定義しようとしたらファイルがなかったので作成しておく。

$ touch app/views/microposts/_micropost.html.erb

app/views/microposts/_micropost.html.erb

<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.
  </span>
</li>

今回の場合は、ユーザーコントローラのコンテキストにおいて、マイクロポストをページネーションしたいため、明示的に@microposts変数を will_paginateに渡す必要があります。

will_paginateについての説明が全くピンと来てないけど明示的に変数を渡す必要があるのは分かった。

チュートリアルのコードにはredirect_toの行のコードがないけどとりあえず下に@microposts変数追加しといた。

app/controllers/users_controller.rb

  def show
    @user = User.find(params[:id])
    redirect_to root_url and return unless @user.activated?
    @microposts = @user.microposts.paginate(page: params[:page])
  end

これですべての要素が揃ったので、プロフィール画面にマイクロポストを表示させてみましょう (リスト11.23)。

マイクロポストの投稿数はuser.microposts.countで取得できて、それなりに高速らしい。いいね!
アイコンの表示の下にマイクロポストを表示させるようにコードを追加。

app/views/users/show.html.erb

  <div class="col-md-8">
    <% if @user.microposts.any? %>
      <h3>Microposts (<%= @user.microposts.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>

ここで、改良した新しいプロフィール画面をブラウザで見てみましょう (図11.5)。
…何とも寂しいページで、がっかりですね。マイクロポストが1つもないのでは無理もありません。

今までのユーザーページと何も変わっていない…!ので動作確認はマイクロポスト追加してからにしよう。

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

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

【148日目】【1日20分のRailsチュートリアル】【第11章】ユーザーが破棄された場合、マイクロポストも破棄されるようにする

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

今日は「11.1.4 マイクロポストを改良する」の「Dependent: destroy」から。

11.1.4 マイクロポストを改良する

Dependent: destroy

サイト管理者はユーザーを破棄する権限を持ちます。ユーザーが破棄された場合、ユーザーのマイクロポストも同様に破棄されるべきです。

マイクロポストとユーザーの関連性を切るのもありだと思ったけど、残しておいても容量圧迫するもんねぇ。。。要らないか。

この振る舞いは、has_manyメソッドにオプションを渡してあげることで実装できます (リスト11.18)。

元々書いてたhas_manyメソッドにdependent: :destroyオプションを追加。
これだけでユーザーを削除したらそのユーザーに紐付いたマイクロポストも削除されるらしい…!楽!シンプル!

app/models/user.rb

class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  :

次に、リスト11.18が正しく動くかどうか、テストを使ってUserモデルを検証してみます。

マイクロポストを生成して、次にユーザーを削除してマイクロポストの数が1減っているかを確認するテスト。

test/models/user_test.rb

  test "associated microposts should be destroyed" do
    @user.save
    @user.microposts.create!(content: "Lorem ipsum")
    assert_difference 'Micropost.count', -1 do
      @user.destroy
    end
  end

問題なく動きました。

$ bundle exec rake test
54 tests, 223 assertions, 0 failures, 0 errors, 0 skips

11.2 マイクロポストを表示する

Twitterのような独立したマイクロポストのindexページは作らずに、図11.4モックアップに示したように、ユーザーのshowページで直接マイクロポストを表示させることにします。

まずは特定のユーザーのマイクロポストを表示させるようにユーザーのプロフィールページを修正する。

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

次は「11.2.1 マイクロポストの描画」から。

【147日目】【1日20分のRailsチュートリアル】【第11章】マイクロポストを特定の順序で取得できるようにする

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

今日は「11.1.4 マイクロポストを改良する」から。

11.1.4 マイクロポストを改良する

具体的には、ユーザーのマイクロポストを特定の順序で取得できるようにしたり、マイクロポストをユーザーに依存させて、ユーザーが削除されたらマイクロポストも自動的に削除されるようにしていきます。

ふむふむ。

デフォルトのスコープ

まずデータベース上の最初のマイクロポストが、fixture内のマイクロポスト (most_recent) と同じであるか検証するテストを書いていきましょう (リスト11.13)。

まずテストを書いてみる。

test/models/micropost_test.rb

  test "order should be most recent first" do
    assert_equal microposts(:most_recent), Micropost.first
  end

リスト11.13では、マイクロポスト用のfixtureファイルからサンプルデータを読み出しているので、次のfixtureファイルも必要になります (リスト11.14)。

マイクロポスト用のfixtureファイルを作成する。

test/fixtures/microposts.yml

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>

tau_manifesto:
  content: "Check out the @tauday site by @mhartl: http://tauday.com"
  created_at: <%= 3.years.ago %>

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

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

ほとんどのシステムでは上から順に作成されるので、fixtureファイルでも意図的に順序をいじっています。
(中略)
この振る舞いは恐らくシステムに依存していて崩れやすいので、(本来は) この振る舞いに依存したテストは書くべきでは無いでしょう。

崩れやすいからオススメではないしいつも使えるわけではない?けど、今回はとりあえず大丈夫なのかな…??

この段階ではマイクロポストをソートしてないのでテストは失敗する。

$ bundle exec rake test TEST=test/models/micropost_test.rb TESTOPTS="--name test_order_should_be_most_recent_first"
Finished in 0.11341s
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

残念ながらデフォルトの順序が昇順となっているので、このままでは数の小さい値から大きい値にソートされてしまいます (最も古い投稿が最初に表示されてしまいます)。
順序を逆にしたい場合は、一段階低いレベルの技術ではありますが、次のように生のSQLを引数に与える必要があります。

新しい投稿から古い投稿の順になるようソートする。

app/models/micropost.rb

  default_scope -> { order(created_at: :desc) }

これでテストが通るようになりました。

$ bundle exec rake test
Finished in 1.94684s
53 tests, 222 assertions, 0 failures, 0 errors, 0 skips

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

次は「11.1.4 マイクロポストを改良する」の「Dependent: destroy」から。