Rails

Rails Tutorial 第13章 簡易まとめ

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

この章ではこれまでに完成させたUserモデルとhas_manyおよびbelongs_toメソッドを使って関連付けを行い、さらに、結果を処理し表示するために必要なフォーム(アップローダー)とその部品を作成します

13.1 Micropostモデル

マイクロポストモデルは完全にテストされ、デフォルトの順序を持ち、また親であるユーザーが破棄された場合には自動的に破棄されるようにします。

いつも通りブランチを切って行こう

$ git checkout -b user-microposts

13.1.1 基本的なモデル

Text型のcontent属性と特定のユーザと関連付けに必要になるuser_idをもたせます

image.png

$ rails generate model Micropost content:text user:references

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

user:referencesという引数も含めていた結果ユーザーと1対1の関係であることを表すbelongs_toのコードも追加されています。
references型を利用すると、自動的にインデックスと外部キー参照付きのuser_idカラムが追加され、UserとMicropostを関連付けする下準備をしてくれます。

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

生成されたマイグレーションファイルにて、user_idとcreated_atカラムにインデックスを付与してください。こうすることで、user_idに関連付けられたすべてのマイクロポストを作成時刻の逆順で取り出しやすくなります
また、user_idとcreated_atの両方を1つの配列に含めている点にも注目です。こうすることでActive Recordは、両方のキーを同時に扱う複合キーインデックス (Multiple Key Index) を作成します。

$ rails db:migrate

13.1.2 Micropostのバリデーション

まずは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 #正常な状態かテスト(Sanity Check)
    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

GREEN

続いてcontent属性に対するバリデーション(存在性、140字以内)、テストを実装

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

注目

$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
app/models/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の関連付け(belongs_to と has_many)

Webアプリケーション用のデータモデルを構築するにあたって、個々のモデル間での関連付けを十分考えておくことが重要です。
今回の場合は
・それぞれのマイクロポストは1人のユーザーと関連付けられ、
・それぞれのユーザーは (潜在的に) 複数のマイクロポストと関連付けられます
image.png
↑ Micropost が belongs_to User

image.png
↑User has_many Micropost

この節で定義するbelongs_to/has_many関連付けを使うことで、下に示すようなメソッドをRailsで使えるようになります
image.png

これらのメソッドは使うと、紐付いているユーザーを通してマイクロポストを作成することができます
新規のマイクロポストがこの方法で作成される場合、user_idは自動的に正しい値に設定されます。

例として下記の様になります

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

という書き方が、次のように書き換えられます。

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

この便利なコードをつかえるようにするためにコードに実装します

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

belongs_toに関しては users: reference を指定した時点でMicropostモデルに設定されている

最後にテストのリファクタリングをして

test/models/micropost_test.rb
 require 'test_helper'

class MicropostTest < ActiveSupport::TestCase

  def setup
    @user = users(:michael)
    @micropost = @user.microposts.build(content: "Lorem ipsum") <--ここ
  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

GREEN

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

この項では、UserとMicropostの関連付けを改良していきます。
具体的には、
1.ユーザーのマイクロポストを特定の順序で取得できるようにする
2.マイクロポストをユーザーに依存させてユーザーが削除されたらマイクロポストも自動的に削除される
です

1番に関して実装していきます
この機能のテストは、見せかけの成功に陥りやすい部分で、「アプリケーション側の実装が本当は間違っているのにテストが成功してしまう」という罠があります。正しいテストを書くために、ここではテスト駆動開発で進めていきます。具体的には、まずデータベース上の最初のマイクロポストが、fixture内のマイクロポスト (most_recent) と同じであるか検証するテストを書いていきましょう

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

テスト用のマイクロポストサンプルデータを作成します

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

※ここでは埋め込みRubyを使ってcreated_atカラムに明示的に値をセットして、順序をあべこべにわざとしている
RED

Default scope

作成時間の逆順、つまり最も新しいマイクロポストを最初に表示するようにしてみましょう。
これを実装するためには、default scopeというテクニックを使います。
このメソッドは、
データベースから要素を取得したときの、デフォルトの順序を指定するメソッドです。
特定の順序にしたい場合は、default_scopeの引数にorderを与えます。例えば、created_atカラムの順にしたい場合は次のようになります。

order(:created_at)

しかしデフォルトが昇順(ascending)になっている為このままでは数の小さい値から大きい値にソートされてしまいます (最も古い投稿が最初に表示されてしまいます)。
順序を逆にしたい場合は、

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

作成日をキーにおいて値を降順(descending)で指定します
(GREEN)

ラムダ式 (Stabby lambda) という文法を使っています。これは、Procやlambda (もしくは無名関数)と呼ばれるオブジェクトを作成する文法です。->というラムダ式は、ブロック を引数に取り、Procオブジェクトを返します。このオブジェクトは、callメソッドが呼ばれたとき、ブロック内の処理を評価します。(詳しくは今度...よくわからない)

dependent:destroy

上で提示した1番の順序にかんしては解決
2番目のユーザーが消えると関連したマイクロポストも削除
にうつります。

この振る舞いは、Userモデルのhas_manyメソッドに結びついている引数:micropostsにオプションを渡してあげることで実装できます

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

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

GREEN

13.2 マイクロポストを表示

今回のアプリではindexページのようなページはつくらず
ユーザーごとのshowページに関連したマイクロポストを表示させます
最初に極めてシンプルなERbテンプレートを作成、次にサンプルデータ生成タスクにマイクロポストのサンプルを追加して、画面にサンプルデータが表示されるようにしてみます。

13.2.1 マイクロポストの描画

ここで一旦動かしやすいようにデータベースをリセット&再インストール、コントローラ生成

$ rails db:migrate:reset
$ rails db:seed
$ rails generate controller Microposts

今回の目的は、ユーザー毎にすべてのマイクロポストを描画できるようにすることです。
ユーザのインデックスでは

<ul class="users">
  <%= render @users %>
</ul>

パーシャルを使って@usersでそれぞれのユーザーを出力していました。これを参考に、_micropost.html.erbパーシャルを使ってマイクロポストのコレクションを表示する

下のコードは1つのマイクロポストを表示するパーシャルである

app/views/microposts/_micropost.html.erb
 <li id="micropost-<%= micropost.id %>"> <--注目1
  <%= 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.<--注目2
  </span>
</li>```

注目1・・・CSS用にidを持たせているが例えば将来、JavaScriptを使って各マイクロポストを操作したくなったときなどに役立ちます
注目2・・・ここではRailsのDateHelperに定義されているtime_ago_in_wordsというヘルパーメソッドを使っています。これはメソッド名の表すとおりですが、「3分前に投稿」といった文字列を出力します。


###マイクロポストのページネーション

ユーザー一覧画面のコードと比較すると、少し違っています。以前は次のように単純なコードでした。

`<%= will_paginate %>`

**引数無し**であるのはUsersコントローラのコンテキストにおいて、@usersインスタンス変数が存在していることを前提としているためです。
今回の場合はUsersコントローラのコンテキストからマイクロポストをページネーションしたいため (つまりコンテキストが異なるため)、明示的に@microposts変数をwill_paginateに渡す必要があります。したがって、そのようなインスタンス変数をUsersコントローラのshowアクションで定義しなければなりません 

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

マイクロポストの関連付けを経由してmicropostテーブルに到達し、必要なマイクロポストのページを引き出してくれている!!!

ということでビューを作成します

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? %> <--注目1
      <h3>Microposts (<%= @user.microposts.count %>)</h3><--注目2
      <ol class="microposts"> <--注目3
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>

注目1・・・ユーザーのマイクロポストが1つも無ければ何も表示しない条件分岐
注目2・・・下記参照
注目3・・・順序なしリストで先程のmicropostパーシャルをよんでいる

countメソッド と lengthメソッド

上のビューにて総投稿数を表示する部分にてcountメソッドではデータベース上のマイクロポストを全部読みだしてから結果の配列に対してlengthを呼ぶ、といった無駄な処理はしていないという点です。そんなことをしたら、マイクロポストの数が増加するにつれて効率が低下してしまいます。そうではなく、(データベース内での計算は高度に最適化されているので) データベースに代わりに計算してもらい、特定のuser_idに紐付いたマイクロポストの数をデータベースに問い合わせています。

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

前項までではマイクロポストの作成が済んでないのでなんとも味気ないプロフィールページになってるのでチャチャと生成してしまおう

すべてのユーザーにマイクロポストを追加しようとすると時間が掛かり過ぎるので、takeメソッドを使って最初の6人だけに追加します。

User.order(:created_at).take(6)

このとき、orderメソッドを経由することで、明示的に最初の (IDが小さい順に) 6人を呼び出すようにしています。
この6人については、1ページの表示限界数 (30) を越えさせるために、それぞれ50個分のマイクロポストを追加するようにしています。また、各投稿内容についてですが、Faker gemにLorem.sentenceという便利なメソッドがあるので、これを使います

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

(ユーザー毎に50個一気に投稿を作成してるわけでなく一つのcontentが作成されたら再度users代入を行っているのでmicropost_idがバラバラに振り分けられるようになっている)

$ 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;
  }
}

image.png

Faker gem

上で使用したFakerはlorem ipsum以外にも、非常に多種多様の事例に対応しています。Fakerのドキュメント (英語) を眺めながら画面に出力する方法を学び、実際に大学名や電話番号、Hipster IpsumやChuck Norris facts (参考: チャック・ノリスの真実) を画面に出力してみましょう。(訳注: もちろん日本語にも対応していて、例えば沖縄らしい用語を出力するfaker-okinawaもhttps://github.com/stympy/faker

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

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

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

プロフィール画面におけるマイクロポストをテストするためには、ユーザーに紐付いたマイクロポストのテスト用データが必要になります。
userにmichaelという値を渡すと、Railsはfixtureファイル内の対応するユーザーを探し出して、(もし見つかれば) マイクロポストに関連付けてくれます。
また、マイクロポストのページネーションをテストするためには、マイクロポスト用のfixtureにいくつかテストデータを追加する必要がありますが、これはユーザーを追加したときと同様に、埋め込みRubyを使うと簡単です。

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 <--注目1

  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'<--注目2
    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

注目1・・・Applicationヘルパーを読み込んだことでfull_titleヘルパーが利用できている点
↓参考

app/helpers/application_helper.rb
 module ApplicationHelper

  # ページごとの完全なタイトルを返します。
  def full_title(page_title = '')
    base_title = "Ruby on Rails Tutorial Sample App"
    if page_title.empty?
      base_title
    else
      page_title + " | " + base_title
    end
  end
end

注目2・・・ネストした文法を使用。h1タグの内側のgravatarクラス付きのimgタグがあるかどうかをチェック

response.body

assert_matchの引数で指定されているが、これにはそのページの完全なHTMLが含まれている。
したがって、そのページのどこかしらにマイクロポストの投稿数が存在するのであれば、探し出してマッチできるはずです。
これはassert_selectよりもずっと抽象的なメソッドです。特に、assert_selectではどのHTMLタグを探すのか伝える必要がありますが、assert_matchメソッドではその必要がない点が違います。

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

次はWeb経由でそれらを作成するためのインターフェイスに取りかかりましょう。
Micropostsリソースへのインターフェイスは、主にプロフィールページとHomeページのコントローラを経由して実行されるので、Micropostsコントローラには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

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

Micropostsコントローラ内のアクセス制御から始めることにしましょう。関連付けられたユーザーを通してマイクロポストにアクセスするので、createアクションやdestroyアクションを利用するユーザーは、ログイン済みでなければなりません

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

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

このテストをパスさせるコードを書くためには少しリファクタリングが必要です

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

Userコントローラ内に定義されていたlogged_in_userをMicropostコントローラでも使える様に
共通コントローラApplication_controllerへ移動させる

これによりMicropostsコントローラからもlogged_in_userメソッドを呼び出せるのでcreateアクションやdestroyアクションに対するアクセス制限が、beforeフィルターで簡単に実装できるようになります

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

  def create
  end

  def destroy
  end
end

GREEN

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

HTTP POSTリクエストをUsersコントローラのcreateアクションに発行するHTMLフォームを作成することで、ユーザーのサインアップを実装しました。
マイクロポストではmicropost/newの代わりにルートパスにフォームを作成することにする

マイクロポストのcreateアクションを作り始めましょう。
こちらでは注目すべきは新しいマイクロポストをbuildするためにUser/Micropost関連付けを使っている点です 。
更にmicropost_paramsでStrong Parametersを使っていることにより、マイクロポストのcontent属性だけがWeb経由で変更可能になっている点に注目してください。

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

マイクロポスト作成フォームは、ログインしている特定のユーザーのコンテキストでのみ機能するので、サイト訪問者がログインしているかどうかに応じて異なるHTMLを提供するコードを使います
これを踏まえた上で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文は自分でリファクタリングしてみるのもいいよね

if文内の2つのパーシャルを実装していく

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メソッドは第一引数の数字に対して適切な第二引数を返す(英語での数の数え方に関するメソッド)
例: 1 micopost , 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 %>

micropost_formパーシャルが動く様にするには2箇所の変更が必要です。
1つは、(以前と同様) 関連付けを使って次のように@micropostを定義することです。

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

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

もうひとつがエラーメッセージのパーシャルを再定義することです。
というのもいままでUser関連の編集をしてきたためエラーメッセージパーシャルにて

app/views/shared/_error_messages.html.erb
 <% 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変数を直接参照していたためです。
今回は@micropost変数を使う必要があります。
これらのケースをまとめると、フォーム変数fをf.objectとすることによって、関連付けられたオブジェクトにアクセスすることができるようにします
@userの部分を object に変更し

```rb: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 %>

パーシャルにオブジェクトを渡すために、値がオブジェクトで、キーがパーシャルでの変数名と同じハッシュを利用します。
言い換えると、form_for(@user)ならerror_messagesパーシャルの中のobjectという変数は@user@micropostなら。。。ということになるので、この変数を使ってエラーメッセージを更新すればよいということです

RED

この変更を受けて、いままでで使われたエラーメッセージパーシャルにもしっかり
フォーム変数f.objectを付け加えること。(じゃないと動かないよ!!)
参考:users/new , users/edit , password_reset/edit

GREEN

13.4 画像のアップロード

ここまででマイクロポストに関する構造は完了したのでこれから画像付きマイクロポストの投稿ができるようにしていく
手順は1、開発環境のβ版を実装 2、いくつかの改善(RE-size等)をとおして本番環境へデプロイ

13.4.1 基本的な画像アップロード

source 'https://rubygems.org'

gem 'rails',                   '5.1.2'
gem 'bcrypt',                  '3.1.11'
gem 'faker',                   '1.7.3'
gem 'carrierwave',             '1.1.0'
gem 'mini_magick',             '4.7.0'
gem 'fog',                     '1.40.0'
gem 'will_paginate',           '3.1.5'
gem 'bootstrap-will_paginate', '1.0.0'
.
.
.

CarrierWaveという画像アップローダーを使います。
まずはcarrierwave gemをGemfileに追加しますがその際あとで必要になるmini_magick gemとfog gemsも含めている。
これらのgemは画像をリサイズしたり (13.4.3)、本番環境で画像をアップロードする (13.4.4) ために使います。
bundle install

CarrierWaveを導入すると、Railsのジェネレーターで画像アップローダーが生成できるようになります。
$ rails g uploader Picture

app/uploaders/picture_uploader.rb
が生成されます

CarrierWaveでアップロードされた画像は、Active Recordモデルの属性と関連付けされているべきです。関連付けされる属性には画像のファイル名が格納されるため、String型にしておきます。

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

image.png

image.png

CarrierWaveに画像と関連付けたモデルを伝えるためには、mount_uploaderというメソッドを使います。このメソッドは、引数に属性名のシンボルと生成されたアップローダーのクラス名を取ります。

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

一旦サーバーを再起動。。

↓ ↓ ↓

マイクロポスト投稿フォームのパーシャルに file_field を設け、strong parmaterもパスさせる

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 %>
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ヘルパーでその画像を描画できるようになります。
また、if文内にて画像の無い (テキストのみの) マイクロポストでは画像を表示させないようにするために、picture?という論理値を返すメソッドを使っている点に注目してください。
このpicture?メソッドは、画像用の属性名に応じて、CarrierWaveが自動的に生成してくれるメソッドです。

image.png

↓画像アップロードのテスト↓

test/integration/microposts_interface_test.rb
 require 'test_helper'

class MicropostInterfaceTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    assert_select 'input[type= 'file']'
    # 無効な送信
    post microposts_path, micropost: { content: "" }
    assert_select 'div#error_explanation'
    # 有効な送信
    content = "This micropost really ties the room together"
    picture = fixture_file_upload('test/fixtures/rails.png', 'image/png')<--注意1
    assert_difference 'Micropost.count', 1 do
      post microposts_path, micropost: { content: content, picture: picture }
    end
    assert assigns[:micropost].picture?<--注意2
    follow_redirect!
    assert_match content, response.body
    # 投稿を削除する
    assert_select 'a', '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

※上のテストを実行する前にテスト用の画像を用意しておかなければならない
ので、
cp app/assets/images/rails.png
test/fixtures/

でfixture内に画像を大本からひっぱておく事

注意1・・・fixture_file_uploadというメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッド
注意2・・・assignsメソッドを使って投稿に成功した後にcreateアクション内のインスタンス変数にアクセス出来るようになる。ここではマイクロポストにアクセスするようになります(インスタンス変数へのアクセスなのにハッシュで引っ張っている)。

13.4.2 画像の検証

ここまでの実装ではアップロードされた画像に対する制限がないため、もしユーザーが巨大なファイルを上げたり、無効なファイルを上げると問題が発生してしまいます。
これから、画像サイズやフォーマットに対するバリデーションを実装し、サーバー用とクライアント (ブラウザ) 用の両方に追加しましょう。

 サーバー側の2つのバリデーション

1、有効な画像の種類を制限

これはCarrierWaveのアップローダーの中に既にヒントがあります。生成されたアップローダーの中にコメントアウトされたコードがありますが、ここのコメントアウトを取り消すことで、画像のファイル名から有効な拡張子 (PNG/GIF/JPEGなど) を検証することができます

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_white_list
    %w(jpg jpeg gif png)
  end
end

↑のコードを有効にする

2、画像のサイズを制御

これはMicropostモデルに書き足していきます。先ほどのバリデーションとは異なり、ファイルサイズに対するバリデーションはRailsの既存のオプション (presenceやlengthなど) にはありません。
したがって、今回は手動でpicture_sizeという独自のバリデーションを定義します

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 <--注目1

  private

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

注目1・・・独自のバリデーションを定義するために、今まで使っていたvalidatesメソッドではなく、validateメソッドを使っている点

注目2・・・引数にシンボル (:picture_size) を取り、そのシンボル名に対応したメソッドを呼び出します。また、呼び出されたpicture_sizeメソッドでは、5MBを上限、それを超えた場合はカスタマイズしたエラーメッセージをerrorsコレクションに追加しています

クライアント側の受け入れ準備

サーバー側で設けたバリデーションをビューに組み込むためにクライアント側で2つの設定を行う

1、バリデーションを反映させる

file_fieldタグにacceptパラメータを付与して使います。

<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>

このときacceptパラメータでは上で許可したファイル形式を、MIMEタイプで指定するようにします。

2、大きすぎるサイズに対して警告を出す

ちょっとしたJavaScript (正確にはjQuery) を書き加えます。こうすることで、長すぎるアップロード時間を防いだり、サーバーへの負荷を抑えたりすることに繋がります。

$('#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.');
  }
});

上のコードでは
CSS idのmicropost_pictureを含んだ要素(=)を見つけ出し、この要素を監視、変化したとき、このjQueryの関数が動き出します。そして、もしファイルサイズが大きすぎた場合、alertメソッドで警告を出すといった仕組み

1,2の追加チェック機能をまとめてマイクロポスト投稿フォームにおとしこみます

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>

13.4.3 画像のリサイズ

ファイルサイズに対するバリデーション (13.4.2) はうまくいきましたが、画像サイズ (縦横の長さ) に対する制限はないので、大きすぎる画像サイズがアップロードされるとレイアウトが壊れてしまいます。
とはいえ、ユーザーに手元で画像サイズを変更させるのは不便なので、画像を表示させる前にサイズを変更する (リサイズする) ようにしてみましょう

今回はImageMagickというプログラムを使いますが、クラウドIDEの場合は以下のコマンドで使用可能。
ローカル開発環境の場合はインストールが必要です

$ sudo apt-get update
$ sudo apt-get install imagemagick --fix-missing

次に、MiniMagickというImageMagickとRubyを繋ぐgemを使って、画像をリサイズしてみましょう
今回はresize_to_limit: [400, 400]という方法を使います。これは、縦横どちらかが400pxを超えていた場合、適切なサイズに縮小するオプションです
ジェネレートしたときに生成されたuploaderファイルに追加します

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_white_list
    %w(jpg jpeg gif png)
  end
end

13.4.4 本番環境へのデプロイ

説明が面倒なので
チュートリアルと
https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-image_upload_in_production

参考↓
https://qiita.com/ryuchun00/items/8e414562b7122e7ec4fb

を基に設定

チュートリアルを進めて行く中で当たったエラー

11/16 tutorial13.4.2を進めている中でサイトではGreenになると記述があるのに
redが発生

コンソール

yuyakuroiwa:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 3752
Started with run options --seed 60465

ERROR["test_profile_display", UsersProfileTest, 0.025043928064405918]
 test_profile_display#UsersProfileTest (0.03s)
NameError:         NameError: uninitialized constant PictureUploader::CarrireWave
            app/uploaders/picture_uploader.rb:2:in `<class:PictureUploader>'
            app/uploaders/picture_uploader.rb:1:in `<top (required)>'
            app/models/micropost.rb:4:in `<class:Micropost>'
            app/models/micropost.rb:1:in `<top (required)>'

/usr/local/rvm/gems/ruby-2.4.0/gems/will_paginate-3.1.5/lib/will_paginate/view_helpers/link_renderer.rb:27: warning: constant ::Fixnum is deprecated
/usr/local/rvm/gems/ruby-2.4.0/gems/will_paginate-3.1.5/lib/will_paginate/view_helpers/link_renderer.rb:91: warning: constant ::Fixnum is deprecated
 FAIL["test_micropost_interfacce", MicropostsInterfaceTest, 1.9180126059800386]
 test_micropost_interfacce#MicropostsInterfaceTest (1.92s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>'

/usr/local/rvm/gems/ruby-2.4.0/gems/will_paginate-3.1.5/lib/will_paginate/view_helpers/link_renderer.rb:27: warning: constant ::Fixnum is deprecated
/usr/local/rvm/gems/ruby-2.4.0/gems/will_paginate-3.1.5/lib/will_paginate/view_helpers/link_renderer.rb:91: warning: constant ::Fixnum is deprecated
 FAIL["test_content_should_be_at_most_140_characters", MicropostTest, 2.109907924197614]
 test_content_should_be_at_most_140_characters#MicropostTest (2.11s)
        Expected true to be nil or false
        test/models/micropost_test.rb:27:in `block in <class:MicropostTest>'

 FAIL["test_user_id_should_be_present", MicropostTest, 2.1254152907058597]
 test_user_id_should_be_present#MicropostTest (2.13s)
        Expected true to be nil or false
        test/models/micropost_test.rb:17:in `block in <class:MicropostTest>'

 FAIL["test_content_should_be_present", MicropostTest, 2.1298170639202]
 test_content_should_be_present#MicropostTest (2.13s)
        Expected true to be nil or false
        test/models/micropost_test.rb:22:in `block in <class:MicropostTest>'

/usr/local/rvm/gems/ruby-2.4.0/gems/will_paginate-3.1.5/lib/will_paginate/view_helpers/link_renderer.rb:27: warning: constant ::Fixnum is deprecated
/usr/local/rvm/gems/ruby-2.4.0/gems/will_paginate-3.1.5/lib/will_paginate/view_helpers/link_renderer.rb:91: warning: constant ::Fixnum is deprecated
/usr/local/rvm/gems/ruby-2.4.0/gems/will_paginate-3.1.5/lib/will_paginate/view_helpers/link_renderer.rb:27: warning: constant ::Fixnum is deprecated
/usr/local/rvm/gems/ruby-2.4.0/gems/will_paginate-3.1.5/lib/will_paginate/view_helpers/link_renderer.rb:91: warning: constant ::Fixnum is deprecated
  58/58: [========================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.60638s
58 tests, 173 assertions, 4 failures, 1 errors, 0 skips

サーバーログ

app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'
app/models/micropost.rb:13:in `picture_size'

結果バリデーション:picture_sizeに誤字脱字があった
そのながれで他のバリデーションも機能せずテストを抜けてしまったと考察
バリデーションを見直して再度テストを走らせるとGREENになった

結構な時間はまってしまったのでメモ

11