0
0

More than 3 years have passed since last update.

rails-tutorial第13章

Last updated at Posted at 2020-06-07

Micropostモデルの作成

投稿はユーザと紐付ける必要があるので、

必要なカラムは、content:text と user_id:integerである。

では、それを作るコマンドを見ていこう

$ rails generate model Micropost content:text user:references

user:referencesをすることで、user_idとidの結びつきを指定することができるらしい。
ショートカットの方法の一つ

そうすると、micropostモデルファイルは、、

app/models/micropost.rb
class Micropost < ApplicationRecord
   belongs_to :user
end

できたマイグレーションファイルにindexを追加する

db/migrate/[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration[5.0]
  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

indexは索引や目次のようなもので、この基準でリソースを参照したい時、よりスピーディーに参照できるようにするもの。

何がやりたいかというと、

私たちがDBにアクセスするときは、user_idといつその投稿が作られたかという基準で頻繁にアクセスしますよーということをDBに伝えて、それに適した設定になるようにしている。

find_byする時に検索するスピードをあげるためのスペースを作りますよーってことらしい。

次は、Userモデルに「たくさんのmicropostsを持てますよー」ということを伝えなければいけない。

なので、

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts
  .
  .
  .
end

となる。これで1対多の関係ができた。

Micropostモデルの実装

モデルのバリデーションに関してはTDDで開発していく。

また、関連付けしたことによって、以下のような便利なメソッドが使えるようになる。

Micropost.find_by(user_id: user_id)
これが
user.microposts

Micropost.new(user_id: user_id)
これが、
user.microposts.build

Micropost.create(user_id: user_id)
これが
user.microposts.create

Micropost.find_by(user_id: ~)
これが
user.microposts.find_by(~~)
というように書き換えることができる。

micropostモデルのテスト

test/models/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

このテストは本来バリデーションで検知して失敗するはずなのに、通ってしまう。
そのため、バリデーションを設定していこう

app/models/micropost.rb
class Micropost < ActiveRecord::Base
  belongs_to :user
  validates :user_id, presence: true
end

さらにmicropostモデルのバリデーションのテストを進める。

test/models/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

  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
end

これでは、空のコンテンツと141文字以上のコンテンツがvalidになってしまうので、バリデーションを設定する。

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

これでテストは通る。

実はテストで、

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

このようなコードがあったが、これは慣習的に正しくない。

関連付けができているので、

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

というように書くのが一般的

この書き方を使うと、

current_user.microposts.buildというようの書くこともできる。

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

1.投稿の表示順序を降順にする。
2.Userが破棄されたら関連する投稿も全て破棄されるようにする。

では、これらの機能を実装していこう。

デフォルトスコープをTDDで実装していく

まずは作られた日時が違う投稿をtest/fixtures/microposts.ymlに定義する

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

これてテスト用のサンプルデータができた。

test/models/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

このテストは、デフォルトスコープが効いてるなら、一番最新の投稿とMicropostの1番目に参照される投稿は同じだよねー?ってテスト。

この状態ではデフォルトスコープ未実装なので失敗する。

では、デフォルトスコープを実装しよう

app/models/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

これでテストが通る。
多分、デフォルトスコープはクエリが呼ばれた時にブロック内に書いたコードがクエリに反映されて、SQL文が発行されるという仕組み?

次は、ユーザーが破棄されたら、それに関連する投稿も破棄されるようにする。

これは簡単で、has_manyメソッドにオプションを渡してあげることで解決する。

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

これにより、ユーザーが破棄されたら投稿も破棄される。

dependent: :destroyのテスト をする。
このオプションはuserモデルのファイルに書くので、テストもuserモデルのテストファイルに書く。

test/models/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

ここで注目したいのが、create!である。
投稿を作れてないまま次のテストに行っても意味がないので、もし投稿が作れなかったときは例外を出すようにする。

この時点では、micropostsコントローラやview、ルーティングも設定してないので、テストが通る。
有効化してなかったら投稿作れないなどに規制がないため。

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

今回注意すべき点は、micropostsを表示するのはUserリソースのviewだということ。

まずは、まだ実装していなかったコントローラを作ろう

$ rails generate controller Microposts

ここではアクションをしていない。
先ほど書いたように、micropostsはUserリソースのviewで使うため、ただ、createアクションやdestroyアクションは今後書き足していく必要がある。

また、micropostsを表示する時のコードを見ていこう

<ol class="microposts">
  <%= render @microposts %>
</ol>

これはUserリソースのindexアクションでやったのと同じだ。

render @リソース名をすると、

_リソース名の単数形 のパーシャルを展開することができる。

これを今回も利用していく。

では、パーシャルを先に作っていこう

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>

link_toの第二引数に入っているmicropost.userを見ていこう。

これはuser_path(micropost.user)の略だよね?
これは関連付けが終わっているからこそできる技。

また、time_ago_in_wordsメソッドに引数を渡すと、
今から逆算して「何日まえに作成されたよー」という情報を参照することができる。

次に、@micropostsはユーザーのプロフィールページで使うため、showアクションの中で参照できるようにする。

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

ここで注目すべきは、最初からpagenateをかけているという点。
params[:page]は自動生成されたような気がする。

次に実際にshowアクションのviewにmicropostsが表示されるようにしていく。

app/views/users/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>

if @user.microposts.any? というコードに注目。

1つも投稿してないというケースがあるので、
.any?メソッドを使って、投稿がある場合という条件を与えている。
これはif errors.any?の時と同じである。

micropostsが存在する時に、_micropostパーシャルを展開するような感じだ。

また、<%= will_paginate @microposts %>には要注意だ。

本来will_pagenateだけだと、書かれているコンテキストをpagenationするのでUserだと勘違いを起こしてしまう。

なので、それを防ぐために、引数に@micropostsを指定して、投稿をページネーションするようにしている。

ただ、このままではユーザーのプロフィールページに投稿が表示されないので、

マイクロポストのサンプルデータを作る。

db/seeds.rbにサンプルデータを定義しよう

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

usersを定義する際にtakeメソッドが使われている。
これは作られたユーザーの最初の6人を意味する。

contentを作る際にLorem.sentence(5)というメソッドが使われている。

そしたら、dbをリセットしてサンプルを作り直す

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

表示されたが、デザインが悪いので、cssを設定する。

app/assets/stylesheets/custom.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;
  }
}

これでデザインが整う

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

$ rails generate integration_test users_profile
統合テストを行う。

次にテスト用のマイクロポストがないと意味ないので、定義する

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

ここで注意すべき点は、ユーザーとの関連付けだ。
user: michael
とすることで、users.ymlのユーザーと関連づけることができる。

これで、統合テストを書いていく。

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

ここで注目すべき点は、
include ApplicationHelper

assert_select 'title', full_title(@user.name)
だ。

applicationhelperにはタイトルを決めるメソッドを定義した。ただ、テストではこれは使えないのでincludeしている。

このままでテストは通る

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

投稿したり表示したりできるようにする。

今回は、投稿用のform_forはuserリソースのshowアクションのviewの中にお邪魔させてもらうので、
newアクションやeditアクションは不要。

必要なのは、createアクションとdestroyアクションということになる。

まずはルーティングを用意する。

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

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

rails g controllerで作られたテストファイルにテストを書いていく。
具体的には適切なユーザーじゃないと使えませんよーというもの。

てか、コントローラーテスト自体そういう目的なんじゃ?

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

これはログインしてないと、createアクションもdestroyアクションも使えませんよーってこと。

この状態ではテストは失敗する。

これの解決法は、

まずlogged_in_user

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

これをuserコントローラに定義してbefore_actionに指定していたのを思い出そう。

これをapplicationコントローラに移せば、micropostsコントローラでもbefore_actionとして使えるんじゃない?って話。

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

  def create
  end

  def destroy
  end
end

これでテストが通る。

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

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

ストロングパラメーターのところは、ユーザーと投稿が関連づけられてるからuser_idを引っ張ってくる必要がないということ?userと関連づけられてるから、勝手にuserのidがuser_idに代入されるということ?

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

if elseでログインしてるかどうかで表示する内容を変えている

また、
<%= render 'shared/user_info' %>
<%= render 'shared/micropost_form' %>

のように、先にパーシャルを用意しておいて、それを展開するようにする

まずはユーザー情報を表示するパーシャルを作っていこう

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>

pluralizeは投稿が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 %>

注目すべきは object: f.object である。

そもそも、エラーメッセージのパーシャルは@userに起きたエラーを表示するものになっていた。
なので、micropostにエラーが起きた時に本来なら表示されないのである。

なので、@userでも@micopostでもどちらでもエラーメッセージが出るようにパーシャルの方をリファクタリングしてやろうという話になる。

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

にある通り、オプションでobject: f.objectと書くとそのオプションがレンダリングされた先でも使えるようになるという特性がある。

そして、f.objectというのはform_forに渡されたオブジェクトという意味なので、userフォームなら@userが、投稿フォームなら@micropostが入るようになる。

これを踏まえてパーシャルを書き換える

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

ただ、このままだとテストは失敗してしまう。
理由は以下

次に、
<%= render 'shared/error_messages', object: f.object %>
投稿フォームの場合は、オプションを指定したが、他にもerror_messageパーシャルをレンダリングしているページがあるので、そこでもオプションを渡すようにする。そうじゃないと、適切にエラーメッセージが表示されない。

具体的に変える必要があるのは

user登録フォーム、編集フォーム、パスワード再設定フォームである。

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>

また、投稿フォームのパーシャルで@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

これでテストが通るようになる。

form_forの引数って?

そもそも、form_forの引数はモデルクラスのインスタンスを指定する。
で、新しく作られていたらcreateアクション、編集されていたらupdateアクションに飛ばすというもの。

フィードを作っていく

マイページの横に自分の投稿と自分がフォローしている投稿が表示されるようにしたい。
それを作っていこう。

まずはfeedメソッドを定義する。定義する場所は、全てのユーザーにfeedを用意したいということから、modelに定義する。

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

    private
    .
    .
    .
end

Micropost.where("user_id = ?", id)に関して、

上の疑問符があることで、SQLクエリに代入する前にidがエスケープされるため、SQLインジェクション (SQL Injection) と呼ばれる深刻なセキュリティホールを避けることができます。この場合のid属性は単なる整数 (すなわちself.idはユーザーのid) であるため危険はありませんが、SQL文に変数を代入する場合は常にエスケープする習慣をぜひ身につけてください。

らしいです。

次にhomeアクションにfeedのインスタンス変数を追加する。

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

  def help
  end

  def about
  end

  def contact
  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の集合体ということ。
このとき、@feed_itemsの各要素がMicropostクラスを持っていたため、RailsはMicropostのパーシャルを呼び出すことができました。このように、Railsは対応する名前のパーシャルを、渡されたリソースのディレクトリ内から探しにいくことができます。

なので,_micropostパーシャルが展開される。

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

ただ、この状態だと問題がある。
投稿に失敗した時にエラーになってしまうのだ。

なぜなら投稿に失敗すると、@feed_itemsがnilになるので、それに対してany?メソッドを呼び出すことはできないからだ。

なので、失敗したケースを考えて、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
      @feed_items = []
      render 'static_pages/home'
    end
  end

  def destroy
  end

  private

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

@feed_items = []を足すことで、もし失敗してもインスタンス変数が存在するから大丈夫。

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

その投稿が自分と紐づいているものならdeleteリンクを表示して削除できるようにする。

まずはそのリンクをマイクロポストパーシャルに追加する。

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

次にdestroyアクションを実装していこう

注意点としては、ログインしていて、さらに、消そうとしている投稿の持ち主という条件付きで動くようにする。

app/controllers/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

def correct_userメソッドに注意。
これはdeleteリクエストを送った時 microposts/:idというurlになることから。

current_userが作ったmicropostsの中にid == params[:id]の投稿がありますか?という条件を作り出している。

また、redirect_to request.referrer || root_urlに注目しよう

ここではrequest.referrerというメソッドを使っています12。このメソッドはフレンドリーフォワーディングのrequest.url変数 (10.2.3) と似ていて、一つ前のURLを返します (今回の場合、Homeページになります)13。このため、マイクロポストがHomeページから削除された場合でもプロフィールページから削除された場合でも、request.referrerを使うことでDELETEリクエストが発行されたページに戻すことができるので、非常に便利です。ちなみに、元に戻すURLが見つからなかった場合でも (例えばテストではnilが返ってくることもあります)、リスト 13.52の||演算子でroot_urlをデフォルトに設定しているため、大丈夫です。

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

まずはfixturesに別々のユーザーに紐づけられた投稿を用意しておく。

test/fixtures/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
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

  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

次にマイクロポストのUIに関する統合テストを書いていく。

test/integration/microposts_interface_test.rb
require 'test_helper'

class MicropostsInterfaceTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    # 無効な送信
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "" } }
    end
    assert_select 'div#error_explanation'
    # 有効な送信
    content = "This micropost really ties the room together"
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost: { content: content } }
    end
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    # 投稿を削除する
    assert_select 'a', text: 'delete'
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(first_micropost)
    end
    # 違うユーザーのプロフィールにアクセス (削除リンクがないことを確認)
    get user_path(users(:archer))
    assert_select 'a', text: 'delete', count: 0
  end
end

マイクロポストの画像投稿

source 'https://rubygems.org'

gem 'rails',                   '5.1.6'
gem 'bcrypt',                  '3.1.12'
gem 'faker',                   '1.7.3'
gem 'carrierwave',             '1.2.2'
gem 'mini_magick',             '4.7.0'
gem 'will_paginate',           '3.1.5'
gem 'bootstrap-will_paginate', '1.0.0'
.
.
.
group :production do
  gem 'pg',  '0.20.0'
  gem 'fog', '1.42'
  end

まずは、carrierwave、mini_magick、fogをインストールする。

CarrierWaveを導入すると、Railsのジェネレーターで画像アップローダーが生成できるようになります。早速、次のコマンドを実行してみましょう

$ rails generate uploader Picture

次にmicropostにカラムを用意する必要がある。
具体的にはpictureカラムに画像ファイル名を保存するstring

$ rails generate migration add_picture_to_microposts picture:string
$ rails db:migrate

次にmicropostと画像を関連付け、pictureカラムを使って管理してねーという指示を出す。
以下

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

次に画像をアップロードするファイルを設置する

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" %>
  <span class="picture">
    <%= f.file_field :picture %>
  </span>
<% end %>

最後にストロングパラメーターを使ってる都合上、pictureカラムの内容も受け取れるように変える。

app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user,   only: :destroy
  .
  .
  .
  private

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

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

ここまでで、画像を入れるまでの処理は完了

次は画像を表示できるようにする

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 %>
    <%= image_tag micropost.picture.url if micropost.picture? %>
  </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>

ここで注目すべきは、
<%= image_tag micropost.picture.url if micropost.picture? %>

picture?というメソッドはgemをインストールしたことで使えるようになる特殊なメソッド。
これを使うと、pictureカラムに参照先があれば、それをimgタグで表示してくれるというもの。

ここまでで、画像の表示もok

画像の加工や検証

このままだと、大きすぎる画像や、画像ファイル以外のものも送れてしまうのでそれを改善したい。

app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
  storage :file

  # アップロードファイルの保存先ディレクトリは上書き可能
  # 下記はデフォルトの保存先  
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # アップロード可能な拡張子のリスト
  def extension_whitelist
    %w(jpg jpeg gif png)
  end
end

まずは、def extension_whitelistの部分のコメントアウトを外す。
そうするとバリデーションが発動し、jpgやjpeg gif png以外のものをアップロードしようとするとバリデーションに引っかかるようになる。

次に画像のサイズのバリデーションをかける

app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  mount_uploader :picture, PictureUploader
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
  validate  :picture_size

  private

    # アップロードされた画像のサイズをバリデーションする
    def picture_size
      if picture.size > 5.megabytes
        errors.add(:picture, "should be less than 5MB")
      end
    end
end

ここで注意したいのが、validateメソッドである。validatesではない。

これを使うと、指定したメソッドをバリデーションとして設定できる。

これで、5メガバイトより大きいサイズをアップロードしようとすると、バリデーションが発動する。

また、フロントエンド側もいじってみる。

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" %>
  <span class="picture">
    <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
  </span>
<% end %>

<script type="text/javascript">
  $('#micropost_picture').bind('change', function() {
    var size_in_megabytes = this.files[0].size/1024/1024;
    if (size_in_megabytes > 5) {
      alert('Maximum file size is 5MB. Please choose a smaller file.');
    }
  });
</script>

<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>に注目していこう。

acceptとやると、指定した拡張子以外はそもそも選択できなくなる。

また、以下の部分にも注目

<script type="text/javascript">
  $('#micropost_picture').bind('change', function() {
    var size_in_megabytes = this.files[0].size/1024/1024;
    if (size_in_megabytes > 5) {
      alert('Maximum file size is 5MB. Please choose a smaller file.');
    }
  });
</script>

これで5メガバイト以上のファイルを選んだ時点で警告が出るようになる。

画像のリサイズ

画像をリサイズするためには、画像を操作するプログラムが必要になります。今回はImageMagickというプログラムを使うので、これを開発環境にインストールします (13.4.4でも説明しますが、本番環境がHerokuであれば、既に本番環境でImageMagickが使えるようになっています)。クラウドIDEでは、次のコマンドでこのプログラムをインストールできます

$ sudo yum install -y ImageMagick

どうやらこれで画像をリサイズするための機能が使えるようになったらしい。
その機能をアップローダーにincludeする

app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  process resize_to_limit: [400, 400]

  storage :file

  # アップロードファイルの保存先ディレクトリは上書き可能
  # 下記はデフォルトの保存先  
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # アップロード可能な拡張子のリスト
  def extension_whitelist
    %w(jpg jpeg gif png)
  end
end

400,400のとこに注目。
例えば、xが400,yが800というファイルだったとする。
そのファイルをアップしようとすると、x200,y400に縮尺を保ったまま小さくすることができる

本番環境での画像アップロード

herokuの場合、アップロードしてもいいけどこちらの都合で勝手に消しますねーという仕様なので、
awsのs3?というものにアップロードする必要があるらしい。

その設定をしていこう

app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  process resize_to_limit: [400, 400]

  if Rails.env.production?
    storage :fog
  else
    storage :file
  end

  # アップロードファイルの保存先ディレクトリは上書き可能
  # 下記はデフォルトの保存先  
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # アップロード可能な拡張子のリスト
  def extension_whitelist
    %w(jpg jpeg gif png)
  end
end

ここからはawsにクレカ登録して金払わなくてはいけないので、動画見る。

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