WeBlog

Webに関する情報を中心に発信してるブログ

carrier_waveとmini_magickを使った画像アップロード

ImageMagicとは?

ImageMagickは、画像のサイズ変更、反転、ミラーリング、回転、変形、せん断、変換、画像の色の調整などができる無料のソフトウェアです。

ImageMagickのインストール

MiniMagickを使うためにImageMagickが必要なのでインストールします。

ターミナルで下記を実行します。

% brew install imagemagick

ImageMagicのバージョンを確認します。

バージョンが表示されたら問題ないです。

% convert --version

Version: ImageMagick 7.0.11-4 Q16 x86_64 2021-03-20 https://imagemagick.org
Copyright: (C) 1999-2021 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenMP(4.5) 
Delegates (built-in): bzlib freetype gslib heic jng jp2 jpeg lcms lqr ltdl lzma openexr png ps tiff webp xml zlib

https://imagemagick.org/

carrierwaveとは?

Rubyアプリケーションからファイルをアップロードするためのgemです。

簡単に画像をアップロードする便利機能を提供してくれているパッケージみたいな感じです。

https://github.com/carrierwaveuploader/carrierwave

mini_magickとは?

MiniMagickとは、画像加工をしてくれるgemです。

MiniMagickを使うには、ImageMagickが必要なのでインストールしました。

https://github.com/minimagick/minimagick

carrierwaveとmini_magickをインストール

Gemfileに'carrierwave''mini_magick'を追加します。

bundle installしてgemをインストールします。

gem 'carrierwave'
gem 'mini_magick'

画像アップローダーを作成するためのコマンドを実行します。

% rails g uploader image
Running via Spring preloader in process 66081
Could not find generator 'uploader'. Maybe you meant 'helper', 'jbuilder' or 'model'
Run `rails generate --help` for more options.

何かエラーになっています。

エラー内容としては、

プロセス66081のSpringプリローダーを介して実行 ジェネレータ「アップローダー」が見つかりませんでした。多分あなたは「メーラー」、「タスク」または「ヘルパー」を意味しました その他のオプションについては、'rails generate --help'を実行してください。

対処法としては、Running via Spring preloader in process 66081の1行でググると対策が出てくるみたい。

下記コマンドを実行してspringを止めるらしい。

% spring stop
rbenv: spring: command not found

https://qiita.com/kohei_wd/items/d4809076df5a3cef5d1a

sprignがないというコマンドが出ました。

対処方として、sudoを使ってspringをインストールすると良いみたい。

sudoはsuper doの略で管理者権限でコマンドを実行するものです。

下記コマンドを実行します。

パスワードが聞かれるので、パソコンのパスワードを入力します。

% sudo gem install spring
Password:
Fetching spring-2.1.1.gem
Successfully installed spring-2.1.1
Parsing documentation for spring-2.1.1
Installing ri documentation for spring-2.1.1
Done installing documentation for spring after 0 seconds
1 gem installed

https://hachimaki37.hatenablog.com/entry/2020/06/17/191914

https://github.com/rails/spring

もう一度spring stopを実行します。

何とか解決です。

% spring stop
Spring stopped.

再度 rails g uploader imageを実行します。

image_uploader.rbというファイルができました。

% rails g uploader image
Running via Spring preloader in process 66607
      create  app/uploaders/image_uploader.rb

マイグレーションファイルを作成

rails gernerateコマンドでテーブルにカラムを追加するためにマイグレーションファイルを作成する。

AddBoardImageToBoardsは、boardsテーブルにboard_imageというカラムを追加するという意味でつけています。

データの方はstring型です。

board_imageというカラムには画像データではなく、画像ファイルの名前を追加します。

% rails g migration AddBoardImageToBoards board_image:string
Running via Spring preloader in process 67337
      invoke  active_record
      create    db/migrate/20210325044126_add_board_image_to_boards.rb

マイグレーションファイルを実行するには、rails db:migrateコマンドを実行します。

ストロングパラメーターにカラム名を追加

ユーザーが画像を送信してきた際に、適切に受け取ることができるよう、ストロングパラメーターにカラム名を追加します。

今回は、:board_imageとします。

:board_image_cacheはモデルのカラムにはないですが、バリデーションに引っかかった際に画像をキャッシュ(残しておく)するために記載します。

def board_params
    params.require(:board).permit(:title, :body, :board_image, :board_image_cache)
end

カラムとアップローダーの関連付け

画像をアップロードするカラムと、rails g uploader imageコマンドで作成したアップローダークラスを紐付けます。

今回は掲示板の画像をアップロードするので、Boardモデルに記載します。

class Board < ApplicationRecord
  # mount_uploader カラム名, アップローダークラス名
  mount_uploader :board_image, ImageUploader
end

MiniMagickで画像を加工する設定

今回はminimagicで画像を加工して、アップロードしたいので、minimagicを使えるように設定します。

minimagicを使えるようにするには、アップローダークラスでincludeします。

minimagicを使って画像を加工する設定は色々ありますが、画像のサイズを加工するが1番メジャーではないかと思います。

https://qiita.com/wann/items/c6d4c3f17b97bb33936f

class ImageUploader < CarrierWave::Uploader::Base
  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  # MiniMagickを使えるように読み込む
    include CarrierWave::MiniMagick

    # アップロードファイルの保存場所を指定(public/ 配下に保存する)
  # Choose what kind of storage to use for this uploader:
  storage :file
  # storage :fog # <= 外部ストレージ(AWSなど)に保存する場合はこっち!!

    # 画像データを保存するパスを設定
  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Provide a default URL as a default if there hasn't been a file uploaded:
  # def default_url(*args)
  #   # For Rails 3.1+ asset pipeline compatibility:
  #   # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
  #
  #   "/images/fallback/" + [version_name, "default.png"].compact.join('_')
  # end

  # Process files as they are uploaded:
  # process scale: [200, 300]
  #
  # def scale(width, height)
  #   # do something
  # end

    # 画像の縦横比を維持したまま、 width を最大 300px、height を最大 200 pxにリサイズする設定
  # Create different versions of your uploaded files:
  version :thumb do
    process resize_to_fit: [300, 200]
  end

  # アップロードできる画像の拡張子を制限する設定
  # Add an allowlist of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_allowlist
    %w(jpg jpeg gif png)
  end

  # Override the filename of the uploaded files:
  # Avoid using model.id or version_name here, see uploader/store.rb for details.
  # def filename
  #   "something.jpg" if original_filename
  # end
end

画像投稿のViewファイルを作成する

Viewファイルは下記のような感じです。

f.file_fieldinput type="file"を作ってくれます。

<%= f.hidden_field :image_cache %>を追記しておくことで、

バリデーションエラーでrenderされた場合でも、画像を残しておくことができます。

# viewファイルの中身
<div class="form-group">
  <%= f.label :board_image %>
  <%= f.file_field :board_image %>
    <%= f.hidden_field :image_cache %>
</div>

# こんなHTMLが出力される
<input type="file" name="board[board_image]" id="board_board_image">

https://github.com/carrierwaveuploader/carrierwave#making-uploads-work-across-form-redisplays

画像データの中身を見てみる

paramsの中にfomから送信された全ての情報が格納されていますが、"board_image"の中に画像のデータが格納されています。

@original_filename="images.jpeg"が送信された画像の名前です。

> params
=> <ActionController::Parameters {"utf8"=>"", "authenticity_token"=>"hVPXVju4pKWgS9KVLu5l3rvBbC2JdFxHSMgw9z8KzUL9dRQqU3NSKe+PP+UGTeFS1WABv53cNMohXgPrOZyHzw==", 
"board"=><ActionController::Parameters {"title"=>"タイトル03", "body"=>"本文03", 
"board_image"=>#<ActionDispatch::Http::UploadedFile:0x00007f92973136d0 @tempfile=#<Tempfile:/var/folders/zh/p27r1mk14l53g68j2p914yvw0000gn/T/RackMultipart20210325-69376-vq81o3.jpeg>, @original_filename="images.jpeg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"board[board_image]\"; filename=\"images.jpeg\"\r\nContent-Type: image/jpeg\r\n">} permitted: false>, 
"commit"=>"登録する", "controller"=>"boards", "action"=>"create"} permitted: false>

画像の保存先

uploades/image_uploader.rbの中身を見てみると下記のような設定があります。

storage :filepublic/に画像が保存されることを表しています。

store_dirメソッドは画像が保存されるパスを示しています。

"uploads/モデル名/カラム名/レコードのid"に保存されます。

   # Choose what kind of storage to use for this uploader:
  storage :file
  # storage :fog

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

DBに保存された画像のパスを取得する

投稿された画像を画面に表示するには、画像のパスがパスが必要です。

モデルにmount_uploader :board_image, ImageUploaderを追加したことで、ImageUploaderクラスのメソッドを使うことができます。

ImageUploaderクラスのメソッドを使うことで、画像のパスを取得することができます。

> board = Board.last
  Board Load (0.2ms)  SELECT  "boards".* FROM "boards" ORDER BY "boards"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<Board id: 24, title: "test03", body: "test03", user_id: 1, created_at: "2021-03-25 08:20:00", updated_at: "2021-03-25 08:20:00", board_image: "images.jpeg">

# オブジェクト名.カラム名.url でuploads配下の画像までのパスを取得できる
> board.board_image.url
=> "/uploads/board/board_image/24/images.jpeg"

# オブジェクト名.カラム名_identifier で画像ファイルの名前を取得できる
> board.board_image_identifier
=> "images.jpeg"

画像を表示する

画像を表示するにはimgタグを生成して、src属性の画像のパスを指定する必要があります。

このimgタグを生成するのがimage_tagメソッドです。

引数に画像のパスを取得する方法で紹介したboard.board_image.urlを書いています。

これでsrc属性にpublic/uploads配下のパスが自動で生成されます。

<%= image_tag board.board_image.url %>

# HTMLは下記のように表示される
<img src="/uploads/board/board_image/24/images.jpeg" >

画像ない場合の表示方法

画像がない場合は画像がないという画像を表示したいです。

その場合に、if文で条件分岐する方法もあります。

下記のように分岐すれば、画像があるかどうかで表示する画像を変えることができます。

<% if board.board_image.present? %>
  <%= image_tag board.board_image.url %>
<% else %>
  <%= image_tag 'no_img.png'%>
<% end %>

しかし、実際が分岐する必要はありません。

アップローダークラスの中にdefault_urlメソッドが用意されています。

このメソッド戻り値にデフォルトで表示したい画像名を指定すると、画像がない場合にdefault_urlで設定した画像のパスがimgタグに設定されます。

def default_url(*args)
    # For Rails 3.1+ asset pipeline compatibility:
    # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))

    # "/images/fallback/" + [version_name, "default.png"].compact.join('_')
    'no_img.png' # <= app/assets/image/no_img.png のパスになる
end

# 下記のようなパスになる
<img class="card-img-top" src="/assets/no_img.png >

参考サイト

https://pikawaka.com/rails/carrierwave

アソシエーションを使って、user_idを外部キーとしてもつオブジェクトを作成する

掲示板などでユーザーがフォームから送信したデータと紐づくように、どのユーザーが投稿したかを外部キーとして持つオブジェクトを作成したい時があります。

例えば、Twitterなどでツイートした情報がどのユーザーが投稿した情報なのか識別できないといけないです。

その際に、外部キーとしてuser_idを持ったオブジェクトを作成しないといけません。

その外部キーカラムに値を設定する方法を書いていきます。

モデルファイル

今回はアソシエーションを使って外部キーに値を設定したいので、アソシエーションをはります。

Userモデルファイルでは、has_manyを使ってアソシエーションを定義しています。

1人のユーザーは複数の投稿をするので、has_many :boardsと書いています。

なので、複数形にしないといけません。

class User < ApplicationRecord
  authenticates_with_sorcery!
  has_many :boards, dependent: :destroy
end

掲示板側から見ると、1つの掲示板は1人のユーザーと紐づいています。

なので、belongs_to :userと書いています。

なので、単数系にしないといけません。

class Board < ApplicationRecord
  belongs_to :user
end

アソシエーションを使って投稿にuser_idを持たせる

ユーザーがフォームからタイトルと本文を送信する場合に、その送信した情報がどのユーザーが投稿したのか情報を持っておく必要があります。

その際に、外部キーとしてuser_idが必要ですが、その外部キーをどのように取得するかです。

思いつく方法としては下記のように書くことができます。

ストロングパラメータを使ってモデルのオブジェクトを作り、後からログインしているユーザーのidを代入する方法です。

@board = Board.new(board_params)
@board.user_id = current_user.id

しかし、アソシエーションをはっているともっと効率的に書くことができます。

下記のように書くことで1行で済みます。

Userモデルにhas_many :boardsというアソシエーションを貼ることで、Userモデルのオブジェクトでboardsというメソッドを呼び出すことができます。

buildメソッドはnewメソッドと役割は同じですが、アソシエーションを使って、オブジェクトを作成していることを明示するためにbuildメソッドを使います。

@board = current_user.boards.build(board_params)

current_user.boardsを実行してみた結果

current_user.boardsを実行するとどうなるのかをrails consoleで試してみます。

現在ログインしているユーザーのidが2番だった場合は、外部キーのuser_idが2番になっているデータを全て取得してきています。

current_user.boardsは現在ログインしているユーザーが投稿したデータを全て取得することができることがわかります。

> current_user.boards

=> [#<Board:0x00007fe702a5c148
  id: 2,
  title: "タイトル2",
  body: "本文2",
  user_id: 2,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00>,
 #<Board:0x00007fe703857ea0
  id: 3,
  title: "タイトル3",
  body: "本文3",
  user_id: 2,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00>,
 #<Board:0x00007fe703857c70
  id: 4,
  title: "タイトル4",
  body: "本文4",
  user_id: 2,
  created_at: Thu, 11 Mar 2021 08:10:46 JST +09:00,
  updated_at: Thu, 11 Mar 2021 08:10:46 JST +09:00>,

以下続く

current_user.boards.buildを実行してみた結果

current_user.boards.buildを実行すると下記のような結果になります。

現在ログインしているユーザーidが2番だっった場合は、current_user.boards.buildでBoardモデルのオブジェクトが作成されて、外部キーであるuser_idに2という値が自動で設定されています。

@board.user_id = current_user.idと書かなくても、自動で設定してくれるのでとても便利です。

> current_user.boards.build

=> #<Board:0x00007fe6f8f9a6c8 id: nil, title: nil, body: nil, user_id: 2, created_at: nil, updated_at: nil>

current_user.boards.build(title: 'タイトル', body: '本文')を実行して結果

current_user.boards.buildの引数にユーザーがフォームから送信した情報を設定してみます。

そうするとBoardモデルのオブジェクトが作成されて、きちんと値が設定されています。

> current_user.boards.build(title: 'タイトル', body: '本文')
=> #<Board:0x00007fe6fe667848 id: nil, title: "タイトル", body: "本文", user_id: 2, created_at: nil, updated_at: nil>

なので、先ほど書いた下記のような書き方で、アソシエーションを使ってモデルのオブジェクトが作成できます。

@board = current_user.boards.build(board_params)

mergeメソッドを使って外部キーに値を設定する

rubyにはmergeメソッドがあります。

https://docs.ruby-lang.org/ja/latest/method/Hash/i/merge.html

このメソッドは連想配列(ハッシュ)に対して、別のハッシュをマージした結果を返します。

なので、これを使うことで外部キーに値を設定することができます。

# ストロングパラメーターを使って、ユーザーがフォームから送信した値を取得
> board_params
=> <ActionController::Parameters {"title"=>"ssss", "body"=>"ssssss"} permitted: true>

# ストロングパラメーターを使って、ユーザーがフォームから送信した値を取得し、mergeメソッドでログインしているユーザーのidをマージする
> board_params.merge(user_id: current_user.id)
=> <ActionController::Parameters {"title"=>"ssss", "body"=>"ssssss", "user_id"=>2} permitted: true>

# mergeした値を使ってモデルのオブジェクトを作る
> board = Board.new(board_params.merge(user_id: current_user.id))
=> #<Board:0x00007fe702714878 id: nil, title: "ssss", body: "ssssss", user_id: 2, created_at: nil, updated_at: nil>

エラーメッセージを表示する

エラーメッセージとは?

アプリケーションの中にはいろんなフォームがあります。

ユーザー登録フォーム、ログインフォーム、プロフィール編集フォーム、ユーザーが何かを投稿するフォーム・・・などいろんな場所でフォームが使われます。

そのフォームでバリデーションに引っかかった際に、何が間違っているのか表示するのがエラーメッセージです。

エラーメッセージを表示するコードはどこに書く?

エラーメッセージは基本的にフォームの上あたりに表示します。

エラーメッセージを表示するコードは、ユーザー登録、ログイン、プロフィール編集、ユーザーが何かを投稿する・・・などいろんな場所で繰り返し使うので、パーシャルテンプレートとして切り出してあげます。

実際に書いてみる

ビューファイルでは、フォームのパーシャルテンプレートを読み込んでいます。

フォームもいろんなとこで使いまわすので、パーシャルテンプレートとして切り出します。

ローカル変数boardインスタンス変数@boardを渡しています。

ビューファイル

<div class="col-lg-8 offset-lg-2">
   <h1>タイトル</h1>
   <%= render 'form', { board: @board } %>
</div>

フォームのパーシャルテンプレート側では<%= render 'error_messages', err_msg: f.object %>で、エラーメッセージ用のパーシャルテンプレートを読み込んでいます。

その際にf.objectをローカル変数err_msgに渡しています。

フォームのパーシャルテンプレート

<%= form_with model: board, local: true do |f| %>
<%= render 'error_messages', err_msg: f.object %>
<div class="form-group">
  <%= f.label :title %>
  <%= f.text_field :title %>
</div>
<div class="form-group">
  <%= f.label :body %>
  <%= f.text_area :body %>
</div>
<%= f.submit %>
<% end %>

エラーメッセージ用のパーシャルテンプレートでは、f.objecterr_msgに渡ってきているので、errorsオブジェクトのany?メソッドを使ってエラーメッセージがあるかどうかを判定しています。

エラーメッセージ用のパーシャルテンプレート

<% if err_msg.errors.any? %>
   <div id="error_messages" class="alert alert-danger">
     <ul class="mb-0">
       <% err_msg.errors.full_messages.each do |msg| %>
         <li><%= msg %></li>
       <% end %>
     </ul>
   </div>
 <% end %>

f.objectの中身を見てみた

ユーザーがtitleとbodyを入力して送信してきた場合です。

フォームのパーシャルテンプレートのfの中身を見てみると、下記のようにいろんな情報が入っています。

その中で@object=#<Board:0x00007fa224c4c200 id: nil, title: "sssss", body: "", user_id: 1, created_at: nil, updated_at: nil>がモデルのオブジェクトであり、ここのエラーメッセージが入ってるはずです。

> f
=> #<ActionView::Helpers::FormBuilder:0x00007fa225a6b980
 @default_html_options={},
 @default_options={:skip_default_ids=>false, :allow_method_names_outside_object=>true},
 @index=nil,
 @multipart=nil,
 @nested_child_index={},
 @object=#<Board:0x00007fa224c4c200 id: nil, title: "sssss", body: "", user_id: 1, created_at: nil, updated_at: nil>,
 @object_name="board",
 @options={:local=>true, :class=>"new_board", :allow_method_names_outside_object=>true, :skip_default_ids=>false},
 @template=
  #<#<Class:0x00007fa21ca91c88>:0x00007fa21cb86580

・・・以下続く

なので、f.objectでobjectメソッドを実行して、インスタンス変数@objectを取得するためにf.object書いています。

any?メソッドの結果もtrueで、f.object.errors.full_messagesでエラーメッセージが取得できています。

> f.object
=> #<Board:0x00007fa224c4c200 id: nil, title: "sssss", body: "", user_id: 1, created_at: nil, updated_at: nil>

> f.object.errors.any?
=> true

> f.object.errors.full_messages
=> ["本文を入力してください"]

部分テンプレートの呼び出し方

部分テンプレートとは何か?

部分テンプレートは同じHTMLの構造を共通化したファイルのこと。

例えば、ブログなどでブログを投稿すると、同じような構造で画面に表示されます。

100回ブログを更新すると、100回同じコードを書かないといけません。

しかし、同じ構造であれば1個の部分テンプレートとしてまとめて、それを読み込むことで使いまわすことができます。

部分テンプレートの呼び出し方

部分テンプレートはアンダースコアから始まるビューファイルです。

このファイルを読み込む場合はいろんな書き方があります。

それを見ていきます。

単数系のインスタンス変数を渡す

ファイル構造は下記のようにします。

/app
  |- /view
      |- /boards
          |- /index.html.erb
          |- /_board.html.erb

ここでは@boardという単数系のインスタンス変数を渡してみます

基本的な形

render partial: 'board', locals: { board: @board }

partial: 'board'で同じ階層上にある_board.html.erbファイルを読み込むように記述しています。

locals:_board.html.erbにあるboardというローカル変数に@boardというインスタンス変数を渡すように設定しています。

省略パターン1

下記のようにpartiallocalsを省略して書くこともできます。

partiallocalsのどちらか一方だけあるのはダメです。

render 'board', board: @board

省略パターン2

render @board

インスタンス変数とファイル名が同じ場合はさらに省略して書くことができます。

この書き方が簡潔でベストだと思います。

複数形のインスタンス変数を渡す

次に@boardsという複数形のインスタンス変数を渡してみます。

基本的な形

<% @boards.each do |board| %>
  <%= render partial: 'board', locals: {board: board} %>
<% end %>

@boardsには複数の掲示板情報が入っているので、ループで回して、1個1個の掲示板を取り出して、部分テンプレートに渡すという書き方です。

良く目にしますが、もっと効率的に書くことができます。

省略パターン1

render partial: 'board', collection: @boards

@boardsには複数の掲示板情報が入っているので、collection: @boardsと書くと、@boardsの中の要素の数だけ_board.html.erbファイルを繰り返し表示することができます。

省略パターン2

render @boards

先ほどのcollection:オプションを使ってループする書き方はさらに省略した書き方ができます。

ファイル名がインスタンス変数@boardsの単数系の場合に使えます。

この場合、@boardsなので、_board.html.erbという部分テンプレートファイルを呼び出す場合に使えます。

このように書くことで、_board.html.erb内で@boardsの中身の要素の数だけ繰り返し表示することができます。

まとめ

いろんな書き方があって混乱しますが、色々試して1個1個確認してみてください。

decoratorの使い方

decoratorとは

ソフトウェアのデザインパターンの一つです。

デザインパターンとは、過去のソフトウェア設計者が発見し編み出した設計ノウハウを蓄積し、名前をつけ、再利用しやすいように特定の規約に従ってカタログ化したものである。

すでに存在する何かのオブジェクトを新しい作るDecoratorオブジェクトでラップ(包み込むこと)することですでに存在する関数やクラスの中身を直接変更することなく、機能を追加したり書き換えたりすることができます。

https://ja.wikipedia.org/wiki/デザインパターン_(ソフトウェア))

Railsでは一般的に

モデル→ビュー

という関係で、モデルからデータを取得して、ビューにモデルから取得したデータを表示します。

decoratorを導入すると、

Model→Decorator→View

という形で「decorator」を間に挟むことができます。

decoratorを挟むことで、モデルから取得したデータをいじってビューで表示することができます。

モデルとビューの役割

例えばデータベースに保存してある「2021-02-28」という日付のデータをモデルを使って取得して、ビューに表示したいと思います。

その際にビュー側では「2018/2/28」とデータの形を変更して表示したいと思います。

「2021-02-28」を「2018/2/28」に変える処理は、モデルに書くべきでしょうか?

それともビューに書くべきでしょうか?

モデルとビューのどちらに書くこともできますが、どちらに書くことも不自然です。

モデルは「データベースを扱う」ことを主な仕事として請け負っています。

なのでモデルは「データベースへのデータの作成・取得・更新・削除」だけを仕事として持っていて欲しいです。

なので、今回のデータを変更す流という処理をモデルに書くのは不自然です。

ビューは「画面への表示を取り扱う」ことを主な仕事にしています。

なのでビューには「画面への表示を取り扱う」ことだけを仕事として持っていて欲しいです。

ビューでデータを加工するのはビューの仕事としても適切ではないです。

そこで、decoratorが登場します。

decoraterは「プログラムで管理しているデータ」を「ユーザーが理解しやすい情報の形」に変換する役割を持たせます。

そうすればモデルとビューのどっちに書く問題を解決できそうです。

まさに

Model→Decorator→View

の関係を作り出せます。

draperとは?

先ほど紹介したdecoratorというデザインパターンを実現する方法としてRailsにはdraperというGemがあります。

decorater部分のコードや動きを作ってくれるのがdraperです。

参考:https://www.ruby-toolbox.com/categories/rails_presenters

draperを使ってみよう

ここからdraperを実際に使ってみます。

draperをインストールする

まずはdraperをインストールする必要があります。

draperにREADMEを確認しながらみてください。

参考:https://github.com/drapergem/draper#accessing-the-model

Gemfileにdraperを書いて、bundle installします。

gem 'draper'

ApplicationDecoratorを作成する

生成されたすべてのデコレータが継承するApplicationDecoratorを作成するには下記を実行します。

これを実行することでのちに出てくるrails generate decoratorコマンドが使えるようになります。

app/decorators配下にdecorater用のファイルができます。

% rails generate draper:install
Running via Spring preloader in process 71394
      create  app/decorators/application_decorator.rb

application_decorator.rbファイルの中身は下記のように何もありません。

application_decorator.rb

class ApplicationDecorator < Draper::Decorator
  # Define methods for all decorated objects.
  # Helpers are accessed through `helpers` (aka `h`). For example:
  #
  #   def percent_amount
  #     h.number_to_percentage object.amount, precision: 2
  #   end
end

decoreterのモデルを作成する。

例えば、すでにUserというモデルのファイルが存在して、そのモデルとdecoreterを対応づけたい場合はrails generate decorator 対応するモデル名を実行します。

対応するモデル名が今回はuserなので、userとしています。

% rails generate decorator user
Running via Spring preloader in process 82830
      create  app/decorators/user_decorator.rb

Userモデルのファイルの中身は下記のようにしています。

usersテーブルのnameカラムにバリデーションを設定しているだけのファイルです。

class User < ApplicationRecord

  validates :name, presence: true, length: { maximum: 255

end

usersテーブルの中身は下記のようにしておきます。

users

rails generate decorator userを実行すると下記のようなファイルが作成されます。

class UserDecorator < Draper::Decorator
  delegate_all

  # Define presentation-specific methods here. Helpers are accessed through
  # `helpers` (aka `h`). You can override attributes, for example:
  #
  #   def created_at
  #     helpers.content_tag :span, class: 'time' do
  #       object.created_at.strftime("%a %m/%d/%y")
  #     end
  #   end

end

delegate_allは対応付けたUserモデルが持っているメソッドをUserDecoratorクラスでも使用できるようにするための記述です。

なので本来は下記のようにnameメソッド使うことができます。

user = User.find(1)
user.name
=> "田中けん"

Userモデルに対応づけたdecoraterを作ると、user.nameなどUserモデルが所有するインスタンスメソッドをUserDecoratorクラスのオブジェクトでも使用することが可能となります。

routes.rbファイル

ルーティングは下記のようにしておきます。

Rails.application.routes.draw do
  resources :users
end

コントローラー

UsersControllerファイルでは下記のように@user = User.find(params[:id])でパラメーターからユーザー情報を取得するようにしておきます。

class UsersController < ApplicationController

  # GET /users/1
  # GET /users/1.json
  def show
        @user = User.find(params[:id])
  end

end

ビューファイル

show.html.erbというビューファイルの中身は下記のようにしておきます。

注目したいのは<%= @user.decorate.full_name %>の部分です。

オブジェクト名.decorate.メソッド名という形になっています。

このメソッド名はdecoraterのメソッドを呼んでいます。

なのでdecorateが必ず必要です。

<!DOCTYPE html>
<html>

<head>
  <title>RunteqNormal</title>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>

  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
</head>

<body>

  <p><%= @user.decorate.created_at %></p>

</body>

</html>

decoraterファイル

user_decorater.rbファイルでは、created_atを作り、その中でcreated_at.strftime("%Y/%-m/%-d")と書いています。

created_at.strftime("%Y/%-m/%-d")がビューで指定した@userオブジェクトのcreated_atメソッドを実行して、取得した値に対してstrftime("%Y/%-m/%-d")メソッドを実行することで、created_atカラムのデータの形を改造してビューに返しているという形です。

class UserDecorator < Draper::Decorator
  delegate_all

  def created_at
    created_at.strftime("%Y/%-m/%-d")
  end

end

なのでユーザー詳細ページのURLにアクセスすると、2021/2/28という形でビューに表示されます。

データベース上では、2021-02-28 09:05:18.637703な形で保存されていたのが、ビューで画面に表示する際は2021/2/28形で表示されるようになります。

これで、データの形を変更するという仕事をモデルやビューに持たせるのではなく、decoratorを使って責務を分けることができました。

decoraterファイルオブジェクト名が無いのはどうして?

user_decorater.rbファイルでcreated_at.strftime("%Y/%-m/%-d")と指定していますが、created_atというゲッターメソッドを呼び出して、created_atを取得しています。

しかし、本来はオブジェクト名.created_at.strftime("%Y/%-m/%-d")と書かないといけないですが、

オブジェクト名がありません。

user_decorater.rbファイルではdelegate_allがあるとオブジェクト名は省略できます。

逆にいうと、delegate_allがないとobject.created_at.strftime("%Y/%-m/%-d")

と書かないとundefined local variable or methodメソッド名'`というエラーになりますので注意が必要です。

要はobjectというdecorater特有のメソッドが、インスタンス自身を指しています。

なので、self.created_at.strftime("%Y/%-m/%-d")としているのと同じです。

selfは自分自身=インスタンス自身を指しています。

フラッシュメッセージの設定

flashメッセージとは

flashメッセージは、ログインに成功・失敗した時やユーザー登録に成功・失敗した時などの、ちょっとしたメッセージを表示してユーザーにお知らせする機能です。

flashメッセージの書き方

flashメッセージは、ログイン画面やユーザー登録画面・・・など、いろんな画面で使い回すので特定のビューファイルに書くのではなく、外部に切り出してあげます。

なので今回は_flash_message.html.erbというフラッシュメッセージを表示するためだけのファイル(部分テンプレート or パーシャルテンプレートという)として切り出してあげています。

【 _flash_message.html.erb 】

<% flash.each do |message_type, message| %>
<div class="alert alert-<%= message_type %>">
  <%= message %>
</div>
<% end %>

先ほど切り出して_flash_message.html.erbをrenderメソッドでレンダリングしています。

レンダリング<%= render %>と書いた部分に指定されたファイルを読み込むというものです。

【 application.html.erb 】

<!DOCTYPE html>
<html>

<head>
  <title>RunteqNormal</title>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>

  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
</head>

<body>

  <%= render "shared/flash_message" %>

  <%= yield %>

</body>

</html>

binding.pryで処理を止めて中身を確認してみると、下記のようにコンソールで表示されます。

きちんんとflash[:success] = "ログインしました"で設定した値がセットされているのがわかります。

このフラッシュメッセージを_flash_message.html.erbで表示をしています。

複数flashメッセージが格納されていても<% flash.each do |message_type, message| %>でループしているので全て表示されます。

【 user_sessions_controller.rb  】

class UserSessionsController < ApplicationController

  def create
    @user = login(params[:email], params[:password])
    if @user
      flash[:success] = "ログインしました"
      redirect_to root_url
    else
      render action: 'new'
    end
  end

end

# ターミナルで表示してみた

    1: <% flash.each do |message_type, message| %>
    2: <% 
    3: binding.pry
    4:  %>
 => 5: <div class="alert alert-<%= message_type %>">
    6:   <%= message %>
    7: </div>
    8: <% end %>

[1] pry(#<#<Class:0x00007fa57b8eedb0>>)> flash
=> #<ActionDispatch::Flash::FlashHash:0x00007fa583c7dc30 @discard=#<Set: {"success"}>, @flashes={"success"=>"ログインしました"}, @now=nil>
[2] pry(#<#<Class:0x00007fa57b8eedb0>>)> message_type
=> "success"
[3] pry(#<#<Class:0x00007fa57b8eedb0>>)> message
=> "ログインしました"

下記ようにredirect_back_or_toメソッドの引数にflash: {success: 'ログインしました'}という形でflashメッセージを設定することもできます。

しかし今回の場合は意図した形で表示されません。

コンソールで中身を表示してみると@flashes={"flash"=>{"success"=>"ログインしました"}になっているんで、message_type"flash"になり、message{"success"=>"ログインしました"}となってしまいます。

これでは_flash_message.html.erbで正しく表示されないので注意が必要です。

【 user_sessions_controller.rb  】

class UserSessionsController < ApplicationController

  def create
    @user = login(params[:email], params[:password])
    if @user
      redirect_to root_url, flash: {success: 'ログインしました'}
    else
      render action: 'new'
    end
  end

end

# ターミナルで表示してみた

    1: <% flash.each do |message_type, message| %>
    2: <% 
    3: binding.pry
    4:  %>
 => 5: <div class="alert alert-<%= message_type %>">
    6:   <%= message %>
    7: </div>
    8: <% end %>

[1] pry(#<#<Class:0x00007fa5837d36f8>>)> flash
=> #<ActionDispatch::Flash::FlashHash:0x00007fa579e6a980 @discard=#<Set: {"flash"}>, @flashes={"flash"=>{"success"=>"ログインしました"}}, @now=nil>
[2] pry(#<#<Class:0x00007fa5837d36f8>>)> message_type
=> "flash"
[3] pry(#<#<Class:0x00007fa5837d36f8>>)> message
=> {"success"=>"ログインしました"}

flashメッセージの仕組み

flashメッセージは先ほどのコントローラーファイルで説明します。

まず、ログインフォームでユーザーが情報を入力し送信ボタンを押します。

そうすると、UserSessionsControllercreateアクションに割り振られます。

createアクション内でログインに成功すると、flash[:success] = "ログインしました"という形でセッションに保存されます。サーバー側ではflash[:success] = "ログインしました"に紐ずく形でセッションIDが割り振られます。

そしてredirect_to root_urlと書いてあるので、root_urlにリダイレクトしてねという形でサーバーからブラウザに302のステータスコードが返されます。

ブラウザでは受け取ったレスポンスの中にあるセッションIDをブラウザのCookieという領域に保存します。

ブラウザはサーバーから支持されたroot_urlに対してリクエストを投げまます。

その際にブラウザのCookieに保存したセッションIDもサーバーに投げます。

受け取ったサーバーは自分が持っているセッションIDとブラウザから送られてきたセッションIDを見比べて誰かを特定します。

セッションIDに紐づく形でflashメッセージが保存されているので、_flash_message.html.erbflashメッセージをHTML内に組み込んでブラウザに返します。

ブラウザでそれを表示するという形でflashメッセージが表示されます。

なんでflashredirect_toでリダイレクトした先で表示されます。

注意点として、1度表示されて、再度どこかのリンクを押すとflashメッセージは削除されます。

renderでflashを使うと不具合が出る

renderメソッドでflashを使うと不具合が出ます。

例えば、ログイン失敗してrenderメソッドで、new.html.erbファイルをレンダリング(表示する/ブラウザに返す)する場合に、flashを使うとnew.html.erbがブラウザに表示された際にflashメッセージは表示されるが、別のページにいくリンクを押しても、リンク先でもflashメッセージが表示されます。

これは、flashはリダイレクト先で1回表示されて、リンクを押したら消えるという仕組みですが、リダイレクトしているわけではないので、2回表示されてしまうことになります。

[ コントローラー ]・・・renderの時はだめ!!

def create
    @user = login(params[:email], params[:password])
    if @user
      flash[:success] = "ログインしました"
      redirect_back_or_to root_url
    else
      flash[:danger] = "ログインに失敗しました"
      render action: 'new'
    end
  end

renderメソッドではflash.nowメソッドを使う

renderメソッドでflashを使うと2回表示されてしまう不具合が出ました。

なのでrenderメソッドではflash.nowを使います。

このメソッドはレンダリングするnew.html.erbで1度だけ表示させることができます。

なので、new.html.erbでどこか別のページに行くリンクを押すとflashメッセージは消えます。

class UserSessionsController < ApplicationController

  def create
    @user = login(params[:email], params[:password])
    if @user
      flash[:success] = 'ログインしました'
      redirect_to root_url
    else
      flash.now[:danger] = 'ログインに失敗しました'
      render action: 'new'
    end
  end

end

add_flash_typesメソッドでflashのキーを指定する

先ほど下記の記載方法では表示されないということを書きました。

これはどうして表示されないかというと、flashでは:notice:alertの2種類のキーしか指定ができないからです。

noticeは何かの通知に、alertは警告のメッセージという風に使い分けます。

class UserSessionsController < ApplicationController

  def create
    @user = login(params[:email], params[:password])
    if @user
      redirect_to root_url, flash: {success: 'ログインしました'}
    else
      render action: 'new'
    end
  end

end

なので下記のように書くと表示することができます。

しかし、全てのflashのキーがnoticealertで指定したいわけではないです。

successdangerといったキーも使いたいです。

その場合にいちいちflash[:success] = "ログインしました"といろんなとこで書いていたのでは大変です。

そこでadd_flash_typesを使います。

class UserSessionsController < ApplicationController

  def create
    @user = login(params[:email], params[:password])
    if @user
      redirect_to root_url, notice: 'ログインしました'
    else
      render action: 'new'
    end
  end

end

add_flash_typesapplication_controller.rbで使います。

書き方は下記のようになります。

class ApplicationController < ActionController::Base
  add_flash_types :success, :danger
end

上記のようにapplication_controller.rbに書いておけば、コントローラー側で下記のように書いても正しく表示されます。

flash: {success: 'ログインしました'}と書いていた部分がsuccess: 'ログインしました'で済みます。

とても便利です。

class UserSessionsController < ApplicationController

  def create
    @user = login(params[:email], params[:password])
    if @user
      redirect_to root_url, success: 'ログインしました'
    else
      render action: 'new'
    end
  end

end

https://qiita.com/Yama-to/items/4d19a714d8bf5bfbabdd

https://api.rubyonrails.org/classes/ActionController/Flash/ClassMethods.html

https://pikawaka.com/rails/flash

i18nの使い方

i18nについて

Railsではデフォルトで英語でいろんな文字が表示されます。

デフォルトでは英語の翻訳ファイルが適用されています。

i18nを使うことで日本語に翻訳したり、英語以外の他の言葉に変えることができます。

i18n日本語化対応手順

i18nの日本語化対応の手順を見ていきます。

i18nのgemを読み込む

Gemfileに下記の1行を追加します。

https://github.com/svenfuchs/rails-i18n

バージョンを指定できるので、上記のURLで適切なバージョンを確認してください。

追加したらbundle installしてください。

gem 'rails-i18n', '~> 5.1'

この1行を追加することで下記のURLの翻訳ファイルが使えるようになります。

このファイルをダウンロードしてconfig/locales配下にセットする方法もありますが、rails-i18n'のgemをインストールすれば、ダウンロードする必要はありません。

https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml

config/application.rbにデフォルトの言語を設定

デフォルトの言語設定を日本語に設定する1行です。

こちらをconfig/application.rbに記載します。

config.i18n.default_locale = :ja

さらにconfig/locales配下にja.ymlという日本語翻訳ファイルをモデル用、ビュー用・・・などと複数に分けて配置することができるので、全てのja.ymlファイルが読み込まれるように設定をします。

下記1行をconfig/application.rbに記載します。

# 言語ファイルを階層ごとに設定するための記述
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]

https://railsguides.jp/i18n.html#railsアプリケーションを国際化向けに設定する

https://railsguides.jp/i18n.html#ロケールファイルの編成

ちなみにサーバーの再起動が必要なので、気をつけてください。

https://ja.stackoverflow.com/questions/8228/railsのファイルの変更の自動読み込みについて

モデル用のja.ymlファイルを作成

config/locales配下にja.ymlファイルを1つ作成するのではなく、モデル用ということで

config/locales/model/ja.ymlという感じで階層構造にします。

そうすれば、modelディレクトリ配下はモデル用の翻訳ファイルだとわかりやすいです。

その中には下記のように記載します。

モデルが増えた場合は、ここにモデルの日本語化やモデルが持つ属性(カラム名)の日本語化を書いていきます。

ymlファイルはインデントで階層構造を記載します。

models

ja: # 日本語か英語問ことで「ja:」「en:」などを書く
  activerecord: # モデルの翻訳ファイルである目印
    models
      user: ユーザー # モデル名: モデルの翻訳
    attributes: # モデルが持つ属性(データ)
      user: # モデル名
        email: メールアドレス # カラム名: 翻訳データ
        password: パスワード
        password_confirmation: パスワード確認

モデルと紐付いたフォームのlabelタグを翻訳する

form_withメソッドの引数にmodel: @userというUserモデルをnewでインスタンス化したオブジェクトが引数で渡っているフォームではconfig/locales/model/ja.ymlに記載した翻訳ファイルの内容をRailsが勝手に見にいって、<label for="email">メールアドレス</label>のようにメールアドレスという日本語にしてくれます。

<%= form_with model: @user, local: true do |f| %>
      <div>
        <%= f.label :email %>
        <%= f.email_field :email %>
      </div>
      <div>
        <%= f.label :password %>
        <%= f.password_field :password %>
      </div>
      <div>
        <%= f.label :password_confirmation %>
        <%= f.password_field :password_confirmation %>
      </div>
<% end %>

ビューファイル用の翻訳ファイルを作成

ビューファイルには例えば<h1>ログイン</h1>link_toメソッドといったタグが存在します。

これらは、モデルと紐ずくものではないので別途ja.ymlファイルを作成して翻訳情報を作成します。

今回はビューの翻訳情報なのでconfig/locales/views/ja.ymlファイルを作成します。

ja:
  users: # app/views/usersディレクトリという意味
    new: # new.html.erbファイルという意味
      user_registration: "ユーザー登録"
      registration: "登録"
      to_login_page: "ログインページへ"

ビューファイルで日本語品役ファイルを読み込む

<%= t('users.new.user_registration') %><%= f.submit t('users.new.registration') %>などは先ほど作ったconfig/locales/views/ja.ymlの日本語翻訳ファイルを見にいくよう明示的に書いています。

form_withの第2引数にモデルクラスのオブジェクトがあると、勝手に翻訳ファイルを見に行ってくれますが、そうでない部分は明示的にこれを見に行けと命令する必要があります。

なので、ビューファイルが増えて日本語化対応しないといけない場合はその都度このファイルに書いていきます。

そうすればモデルと紐付かないビューファイルの部分は、このファイルでまとめることで管理できます。

<h1><%= t('users.new.user_registration') %></h1>
<%= form_with model: @user, local: true do |f| %>
      <div>
        <%= f.label :email %>
        <%= f.email_field :email %>
      </div>
      <div>
        <%= f.label :password %>
        <%= f.password_field :password %>
      </div>
      <div>
        <%= f.label :password_confirmation %>
        <%= f.password_field :password_confirmation %>
      </div>
            <%= f.submit t('users.new.registration') %>
<% end %>
<div>
  <%= link_to t('users.new.to_login_page'), login_path %>
</div>