LoginSignup
0
0

More than 1 year has passed since last update.

Ruby on Rails チュートリアル第13章をやってみて

Posted at

ユーザーのマイクロポスト

■第13章
Twitterのミニクローンを完成させる。

13.1 Micropostモデル

Micropostリソースの最も本質的な部分を表現するMicropostモデルを作成する。

13.1.1 基本的なモデル

まずはMicropostモデルを生成。

rails generate model Micropost content:text user:references

ApplicationRecordを継承したモデルが作られる。そして、belongs_toのコードも自動的に追加されている。
理由は、user:referencesという引数も含めていたため。

自動生成されたMicropostモデル。

micropost.rb
class Micropost < ApplicationRecord
   belongs_to :user
end

Userモデルとの最大の違いはreferences型を利用している点。
これを利用すると、自動的にインデックスと外部キー参照つきのuser_idカラムが追加され、UserとMicropostを関連付けする下準備をしてくれる。

[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration[5.1]
  def change
    create_table :microposts do |t|
      t.text :content
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at]
  end
end

add_indexによって、作成時刻の逆順で取り出しやすくなる。

13.1.2 Micropostのバリデーション

バリデーションを追加していく。まずはMicropostモデル単体を動くようにしてみます。

setupでは、fixtureのサンプルユーザーと紐づけた新しいマイクロポストを作成している。

次に、マイクロポストが有効であるかどうかをチェックする。

最後に、user_idの存在性のバリデーションに対するテストも追加する。

有効性に対するテスト。

micropost_test.rb
require 'test_helper'

class MicropostTest < ActiveSupport::TestCase

  def setup
    @user = users(:michael)
    # このコードは慣習的に正しくない
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
  end

  test "should be valid" do
    assert @micropost.valid?
  end

  test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end
end

setupの中は実は慣習的には正しくないが、後で修正する。

1つ目のテストでは、正常な状態かどうかをテストしている。2つ目のテストでは、user_idが存在しているかどうかをテストしている。このテストをパスせさるために、存在性のバリデーションを追加してみる。

micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
end

次に、content属性に対するバリデーションを追加する。140文字より長くないように制限を加える。

テストを作成。

micropost_test.rb
  test "content should be present" do
    @micropost.content = "   "
    assert_not @micropost.valid?
  end

  test "content should be at most 140 characters" do
    @micropost.content = "a" * 141
    assert_not @micropost.valid?
  end

そしてバリデーションを追加。

micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

テストはなぜか手こずりましたが、無事GREENに。

13.1.3 User/Micropostの関連付け

1つのツイートは1人のユーザーのみ持っていて、1人のユーザーは複数のツイートを持ってる。

user.microposts.create
user.microposts.create!
user.microposts.build

これらのメソッドは使うと、紐付いているユーザーを通してマイクロポストを作成することができる。

さっき書いた

@user = users(:michael)
# このコードは慣習的に正しくない
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)

をこんな風に書くことができる。

@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")

@user.microposts.buildのようなコードを使うためには、 UserモデルとMicropostモデルをそれぞれ更新して、関連付ける必要がある。
Userモデルはhas_many :micropostsと追加する。

micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end
user.rb
class User < ApplicationRecord
  has_many :microposts
  .
  .
  .
end

関連付けできたら、setupメソッドを修正する。

  def setup
    @user = users(:michael)
    @micropost = @user.microposts.build(content: "Lorem ipsum")
  end

テストも無事GREEN。

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

UserとMicropostの関連付けを改良していく。

最も新しいマイクロポストを最初に表示できるようにする。これを実装するためには、default scopeというテクニックを使う。

マイクロポストの順序付けをテスト。

micropost_test.rb
require 'test_helper'

class MicropostTest < ActiveSupport::TestCase
  .
  .
  .
  test "order should be most recent first" do
    assert_equal microposts(:most_recent), Micropost.first
  end
end

fixtureファイルも作成しておく。

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

created_atカラムは基本的に手動で更新できないが、fixtureファイルの中では更新可能。

試しにテストしてみるとREDに。

古いバージョンのRailsでは、欲しい振る舞いにするためには生のSQLを書くしか選択肢がなかったが、Rails 4.0からは次のようにRubyの文法でも書けるようになったらしい。

デフォルトスコープを変更する書き方が以下のコード。これによって新規作成順に並ぶ。

micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

これでテストをやると無事通る。

ユーザーが削除されたらマイクロポストも削除されるようにする方法は、実はかなり簡単。一行追加するだけ。

user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  .
  .
  .
end

dependent: :destroyを使うと。持ち主のいないMicropostがデータベース上に残り続けるという問題を解決することができる。

そのテストを追加する。

user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  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
end

テストもGREENに。

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

マイクロポストの表示とテスト部分を作る。

twitterのような独立したマイクロポストのindexページは作らずに、ユーザーのshowページで直接マイクロポストを表示してみる。

13.2.1 マイクロポストの描画

一度データベースをリセットし、サンプルデータを再生成しておく。

$ rails db:migrate:reset
$ rails db:seed

Micropostsコントローラを作っておく。

$ rails generate controller 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>

time_ago_in_wordsというヘルパーメソッドで「○分前に投稿」みたいな文字列を出力できる。

ページネーション用にshow@micropostsを作成する。

users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.paginate(page: params[:page])
  end
  .
  .
  .
end

プロフィール画面にマイクロポストを表示させる。

show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
  </aside>
  <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>
</div>

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

データベースに6人分のマイクロポストを作る。

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

ここでいつものようにサンプルデータを生成。

$ rails db:migrate:reset
$ rails db:seed

あとはCSSの見た目を整えて終わり。

13.2.3 プロフィール画面のマイクロポストをテストする

プロフィール画面で表示されるマイクロポストに対して、統合テストを書いていく。統合テストを作成する。

$ rails generate integration_test users_profile

fixtureを書いていく。

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、マイクロポストの投稿数、そしてページ分割されたマイクロポスト、といった順でテストしていく。

full_titleヘルパーが利用できている。

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_select 'h1>img.gravatar'

このように書くことで、h1タグの内側にある、gravatarクラス付きのimgタグがあるかどうかをチェックできる。

テストも通りました。

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

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

最後に、ユーザーがマイクロポストをWeb経由で破棄できるようにする。

マイクロポストリソースのルーティング。

routes.rb
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'
  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 マイクロポストのアクセス制御

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

まずはテストを書いていく。

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

このテストをパスするために、少しアプリケーション側のコードをリファクタリングする必要がある。

logged_in_userメソッドがMicropostsコントローラでも必要。各コントローラが継承するApplicationコントローラに、このメソッドを移していく。

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を削除しておく。これでlogged_in_userメソッドを呼び出せるようになった。

Micropostsコントローラの各アクションに認可を追加。

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

  def create
  end

  def destroy
  end
end

テストも無事通りました。

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

マイクロポスト作成画面を作っていく。ユーザーのログイン状況に応じて、ホーム画面の表示を変更することを目標にする。

マイクロポストのcreateアクションを作り始める。

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.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 %>
  <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 "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
  </div>

  <%= link_to image_tag("rails.png", alt: "Rails logo"),
              'http://rubyonrails.org/' %>
<% end %>

いくつかパーシャルを作る必要がある。まずはHomeの新しいサイドバーから作成。

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

次はマイクロポスト作成フォーム。

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

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

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

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

Userオブジェクト以外でも動作するようにerror_messagesパーシャルを更新する。

_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パーシャルが他の場所でも使われているため。

修正したら、無事テストはGREENに。

13.3.3 フィードの原型

フィードの原型を作る。

feedメソッドの作成。

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

?をつけるのはSQLインジェクションを避けるため。

homeアクションにフィードのインスタンス変数を追加する。

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

ステータスフィードのパーシャルも作成。

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

そして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>

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

最後の機能として、マイクロポストを削除する機能を追加する。そのためのリンクを追加する。

マイクロポストのパーシャルに削除リンクを追加する。

_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.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>

次に、Micropostsコントローラのdestroyアクションを定義し、現在のユーザーが削除対象のマイクロポストを保有しているかどうかを確認する。

microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user,   only: :destroy
  .
  .
  .
  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

request.referrerメソッドは、DELETEリクエストが発行されたページに戻すことができるので非常に便利らしい。

13.3.5 フィード画面のマイクロポストをテストする

fixtureを作成していく。別々のユーザーに紐付けられたマイクロポストを追加していく。

microposts.yml
ants:
  content: "Oh, is that what you want? Because that's how you get ants!"
  created_at: <%= 2.years.ago %>
  user: archer

zone:
  content: "Danger zone!"
  created_at: <%= 3.days.ago %>
  user: archer

tone:
  content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
  created_at: <%= 10.minutes.ago %>
  user: lana

van:
  content: "Dude, this van's, like, rolling probable cause."
  created_at: <%= 4.hours.ago %>
  user: lana

自分以外のユーザーのマイクロポストは削除をしようとすると、適切にリダイレクトされることをテストで確認する。

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

  test "should redirect destroy for wrong micropost" do
    log_in_as(users(:michael))
    micropost = microposts(:ants)
    assert_no_difference 'Micropost.count' do
      delete micropost_path(micropost)
    end
    assert_redirected_to root_url
  end
end

ここで統合テストを作成。

$ rails generate integration_test microposts_interface

テストの中身を記述して、無事GREENに。

感想

この章はとにかく長かったです。データモデルの関連付けのあたりが業務でも大事になってくるのかなーと感じました。

あと、11章で手こずったHerokuへのアドオンがうまくできたので何よりです。。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0