静的型付け言語原理主義者によるRailsチュートリアル(第13章)

image

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

  • ユーザーが短いメッセージを投稿できるようにするためのリソースであるマイクロポストを追加していく
    • Micropost データモデルを作成
    • User モデルと has_many および belong_to メソッドを使って関連付けを行う
    • 結果を処理し表示するために必要なフォームとその部品を作成する

13.1 Micropost モデル

  • まずはモデルを作成するところから

13.1.1 基本的なモデル

  • マイクロポストの内容を保存する content 属性と、特定のユーザーとマイクロポストを関連付ける user_id 属性の2つの属性だけを持つ Micropost モデルを生成する

    rails generate model Micropost content:text user:references
  • user:reference の引数により、生成されたモデルには belongs_to のコードも追加されている

  • reference 型を使うことにより、自動的にインデックスと外部キー参照付きの user_id カラムが追加され、User と Micropost を関連付ける下準備をしてくれる

    • つよつよでは
  • user_id に関連付けられたすべてのマイクロポストを作成時刻の逆順で取り出しやすくするために、user_idcreated_at カラムにインデックスを付与する

    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
  • 以上をもってマイグレーションを実施

13.1.2 Micropost のマイグレーション

  • まずは Micropost モデル単体を TDD で動くようにしてみる
  • setup で fixture のサンプルユーザーと紐付けた新しいマイクロポストを作成し、その有効性をチェックしする

    • あらゆるマイクロポストはユーザーの id を持っているべきなので、user_id の存在性バリデーションに対するテストも追加する

      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
  • 1つ目のテストでは正常な状態かどうかをテストしている

  • 2つ目のテストでは user_id が存在しているかどうかをテストしている

    • パスするために存在性のバリデーションを追加する

      class Micropost < ActiveRecord::Base
      belongs_to :user
      validates :user_id, presence: true
      end
  • Rails5 では上記バリデーションを追加せずともテストが通ってしまう

    • 慣習的に正しくないコードを書いた場合のみ発生する
      • そんな慣習に依存する仕様はどうなのよ。。。
  • 次にマイクロポストの content 属性に対するバリデーションを追加する

    • 存在必然性の制限と、140文字より長くならないようにする制限を加える

      • TDD スタイルで

        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
  • アプリケーション側の実装を対応させる

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

13.1.3 User/Micropost の関連付け

  • それぞれのマイクロポストは1人のユーザーと関連付けられ、それぞれのユーザーは複数のマイクロポストと関連付けられる
  • マイクロポストがユーザーに所属するための関連付けはマイグレーションによって自動生成されているが、ユーザーがマイクロポストを複数所有できるようにする関連付けは手動で行う必要がある

    class User < ApplicationRecord
    has_many :microposts
    .
    .
    .
    end
  • 正しく関連付けができたら、慣習的に正しくマイクロポストを作成する

    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

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

  • User と Micropost の関連付けを改良していく
    • ユーザーのマイクロポストを特定の順序で取得できるようにする
    • マイクロポストをユーザーに依存させ、ユーザー削除に伴ってマイクロポストも自動削除されるようにする

デフォルトのスコープ

  • user.microposts メソッドはデフォルトでは読み出しの順序に対して何も保証しないので、最新のものを先頭表示するようにしてみる
  • 例によって TDD で

    • DB 上の最初のマイクロポストが fixture 内のマイクロポストと同じであるか

      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 ファイルも用意しておく

  • この状態ではテストが失敗するので、default_scope でマイクロポストの順序を変更する

    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

Dependent: destroy

  • サイト管理者はユーザーを廃棄する権限を持つので、ユーザーのは気に伴ってマイクロポストも破棄する

    class User < ApplicationRecord
    has_many :microposts, dependent: :destroy
    .
    .
    .
    end
  • dependent: :destroy オプションを使うと、ユーザー削除時にそのユーザーに紐付いたマイクロポストも一緒に削除されるようになる

    • DB に所有者不明のマイクロポストが残ってしまうのを防ぐ
      • こういう不正データ対応は大事よね。。。
  • dependent: :destroy が機能するか User モデルを検証する

    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

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

  • ユーザーの show ページで直接マイクロポストを表示させる

13.2.1 マイクロポストの描画

  • まずは Micropost のコントローラとビューを作成するために、コントローラを生成する
  • 1つのマイクロポストを表示するパーシャルは以下のようになる

    <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 変数を Users コントローラの show アクションで明示的に will_paginate に渡す

      class UsersController < ApplicationController
      .
      .
      .
      def show
      @user = User.find(params[:id])
      @microposts = @user.microposts.paginate(page: params[:page])
      end
      .
      .
      .
      end
  • 最後に投稿数表示を加味してマイクロポストをユーザーの show ページに追加する

    <% 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 マイクロポストのサンプル

  • サンプルデータにマイクロポストを追加する

    .
    .
    users = User.order(:created_at).take(6)
    50.times do
    content = Faker::Lorem.sentence(5)
    users.each { |user| user.microposts.create!(content: content) }
    end
  • 開発環境用の DB で再度サンプルデータを生成する

  • サンプルデータから生成されたページにはマイクロポスト固有のスタイルが与えられていないので、CSSを適用する

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

  • プロフィール画面で表示されるマイクロポストに対して統合テストを書いていく
  • プロフィール画面におけるマイクロポストをテストするために、ユーザーと関連付けられたマイクロポストの fixture を追加する

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

      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

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

  • データモデリングとマイクロポスト表示テンプレートが完成したので、次は Web 経由でそれらを作成するためのインターフェイスに取り掛かる
  • Micropost リソースへのインターフェースは、主にプロフィールページと Home ページのコントローラを経由して実行されるので、Micropost コントローラには newedit のようなアクションは不要になる

    • createdestroy があれば十分なので、マイクロポストリソースのルーティングは以下のようになる

      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 アクションを利用するユーザーはログイン済みでなければならない
  • ログイン済みかどうかを確かめるテストでは、正しいリクエストを各アクションに向けて発行し、マイクロポストの数が変化していないかどうか、またリダイレクトされていないかどうかを確かめればよい

    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 メソッドを Application コントローラに移す

    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 を削除する

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

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

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

  • micropost/new ページを使う代わりに、ホーム画面(ルートパス)にフォームを置いてマイクロポストを作成する
  • マイクロポストの create アクションを作る

    • 新しいマイクロポストを build するために User/Micropost 関連付けを使用する

      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 を提供する

    <% 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 %>
  • 上記コードを動かすためにサイドバーで表示するユーザー情報のパーシャルを作成する

    <%= 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>
  • マイクロポスト作成フォームも定義する

    <%= 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 アクションにマイクロポストのインスタンス変数を追加する

    class StaticPagesController < ApplicationController
    
    def home
    @micropost = current_user.microposts.build if logged_in?
    end
    
    def help
    end
    
    def about
    end
    
    def contact
    end
    end
  • @micropost 変数はログインしているときのみ定義される

  • マイクロポスト投稿フォームのパーシャルを動かすために、エラーメッセージのパーシャルを再定義する必要もある

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

      <% 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 %>
  • このパーシャルは他の場所でも使用されているので、テストは失敗する

    • ユーザー登録、パスワード再設定、ユーザー編集のそれぞれのビューを更新すればテストは通る

13.3.3 フィードの原型

  • Home ページにマイクロポストを表示する部分が実装されていないので、まだ投稿内容をすぐに見ることはできない
  • 投稿後にマイクロポストをフィードを表示する
  • User モデルに feed メソッドを作り、マイクロポストのステータスフィードを実装する準備を行う

    class User < ApplicationRecord
    .
    .
    .
    # 試作feedの定義
    # 完全な実装は次章の「ユーザーをフォローする」を参照
    def feed
    Micropost.where("user_id = ?", id)
    end
    
    private
    .
    .
    .
    end
  • フィード機能導入のため、home アクションにフィードのインスタンス変数を追加する

    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
  • Home ページのフィード用パーシャルは以下

    <% 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>
    <% else %>
    .
    .
    .
    <% end %>
  • マイクロポスト投稿に失敗すると Home ページが @feed_items インスタンスを保持しているので壊れてしまう

    • 空の配列を渡しておけば回避できる

      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

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

  • 自分が投稿したマイクロポストに対してのみ削除リンクが動作するようにする
  • マイクロポストのパーシャルに削除リンクを追加する

    <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>
  • 次に Micropost コントローラの destroy アクションを定義する

    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

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

  • マイクロポスト用の fixture に別々のユーザーに紐付けられたマイクロポストを追加していく
  • 次に自分以外のユーザーのマイクロポストを削除しようとすると、適切にリダイレクトされることを確認する

    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に対する統合テストを書く

    • ログイン
    • マイクロポストのページ分割の確認
    • 無効なマイクロポストを投稿
    • 有効なマイクロポストを投稿
    • マイクロポストの削除
    • 他のユーザーのマイクロポストには[delete]リンクが表示されないことを確認

      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

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

  • 画像つきマイクロポストを投稿できるようにする
    • 画像アップロード用フォームと投稿された画像そのものという2つの視覚的要素が必要

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

  • CarrierWave という画像アップローダーを Gemfile に追加していつものように bundle install
    • 失敗するので gem foggem fog-aws にしてゴリ押しで進める
  • CarrierWave により Rails のジェネレーターで画像アップローダーが生成できる
  • 必要となる picture 属性を Micropost モデルに追加するためにマイグレーションファイルを生成して開発環境の DB に適用する
  • Micropost モデルにアップローダーを追加する

    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
  • マイクロポスト投稿フォームに画像アップローダーを追加する

    <%= 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 %>
  • 最後に Web から更新できる許可リストに picture 属性を追加する

    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
  • さらに、マイクロポストの画像表示を追加する

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

13.4.2 画像の検証

  • 画像サイズやフォーマットに対するバリデーションを実装する
  • 画像フォーマットのバリデーションは CarrierWave を修正する

    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
  • 画像サイズについては Micropost モデルにバリデーションを追加する

    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
  • フロント側にもバリデーションを追加する

    • 拡張子チェックを追加
    • ファイルサイズは jQuery でチェックする

      • 令和になっても jQuery を使うときが来るとはな…

        <%= 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 画像のリサイズ

  • 画像の縦横の長さに対する制限はないので、画像を表示させる前にリサイズする
  • ImageMagick を入れる
    • ImageMagick かぁ。。。これがデファクトなのかな?
  • 画像をリサイズするために画像アップローダーを修正する

    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

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

  • 本番環境での画像アップロードを調整する

    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
  • 画像アップロード先として S3 のセットアップが必要なのでスキップ

13.5 最後に

  • いつもの通り master にマージして heroku にデプロイして終了

所感

いよいよ Rails チュートリアルも終盤であるが、ここまでくると Web アプリ開発として目新しいことはあまりなく、Rails の機能をただ扱うだけになってきたので正直なところモチベーションの維持が難しくなってきた。(まあ Rails のためのチュートリアルなので何も間違っていないのだが) 次が最後の章なのでなんとか完走したい。。。


6276 Words

2020-04-30 12:32 +0000