基礎Ruby on Rails #23 Chapter13 複数画像のアップロード／順番の入れ替え 

前 [基礎Ruby on Rails #22 Chapter13 ファイルのアップロード](https://qiita.com/tseno/items/6abbda40954e4c2a541a)
次 [基礎Ruby on Rails #24 Chapter13 クラウドスレレージサービス（Amazon S3）の利用](https://qiita.com/tseno/items/d70a82609d80f7a2e940)

# ブログ画像のアップロード

## EntryImageクラスの作成

- Active Storageにはモデルと複数個のファイルを結びつけるためのクラスメソッド`has_many_attached`が用意されているが、ファイルの一部を差し替えたり、順番を入れ替えたりできないという制約がある。
- そこで、個々のブログ写真を添付するためのモデル`EntryImage`を作成する。(1つのEntryレコードに、複数のEntryImageレコードがぶら下がる)

```sh
$ bin/rails g model entry_image
      invoke  active_record
      create    db/migrate/20181020120055_create_entry_images.rb
      create    app/models/entry_image.rb
```

- 作成されたマイグレーションスクリプトを以下のように3項目追加する。

```rb:db/migrate/20181020120055_create_entry_images.rb
class CreateEntryImages < ActiveRecord::Migration[5.2]
  def change
    create_table :entry_images do |t|
      t.references :entry     # 外部キー
      t.string :alt_text, null: false, default: ""    # 代替テキスト
      t.integer :position     # 表示位置

      t.timestamps
    end
  end
end
```

- マイグレーションを実行する。

```sh
$ bin/rails db:migrate
== 20181020120055 CreateEntryImages: migrating ================================
-- create_table(:entry_images)
   -> 0.0021s
== 20181020120055 CreateEntryImages: migrated (0.0024s) =======================
```

- EntryImagesと、プロフィール画像が登録できるMemberモデルと比較すると以下の部分が異なる。
    - 削除のチェックボックスとその項目がない→レコードごと削除するので不要
    - 空禁止のバリデーションがある(createのみ)→プロフィール画像は空も可で、対してこちらは不要なときはレコードごと削除するため空で登録することがない。createのみなのは代替テキストのみ変更する場合があるため。
    - 削除処理をコントローラーのdestroyに書き、こちらに書かないため保存するのがシンプル。
- EntryImageモデルを以下のように書き換える。

```rb:app/models/entry_image.rb
class EntryImage < ApplicationRecord
  belongs_to :entry
  has_one_attached :data

  attribute :new_data

  validates :new_data, presence: { on: create }

  validate if: :new_data do
    if new_data.respond_to?(:content_type)
      unless new_data.content_type.in?(ALLOWED_CONTENT_TYPES)
        errors.add(:new_data, :invalid_image_type)
      end
    else
      errors.add(:new_data, :invalid)
    end
  end

  before_save do
    self.data = new_data if new_data
  end
end
```

- Entryモデルに、一対多である`has_many`を追加する。
- entry.imagesでブログ記事の写真一覧が取得できる。

```rb:app/models/entry.rb（一部）
  has_many :images, class_name: "EntryImage"
```

## 画像のアップロードと削除

### ルーティングの設定

- ブログ画像は1つのブログに結び付けられるので、ネストしたルーティングとして登録する。
- 以下のように、controllerオプションを用いると、リソース名とコントローラ名の間を自由に関連付けることができる。

```rb:config/routes.rb（一部）
  resources :entries do
    resources :images, controller: "entry_images"
  end
```

|アクション|パス|HTTPメソッド|パスを返すメソッド|
|:--|:--|:--|:--|
|index|/entries/123/images|GET|entry_images_path(entry)|
|show|/entries/123/images/1|GET|entry_image_path(entry,image)|
|new|/entries/123/images/2/new|GET|new_entry_image_path(entry)|
|edit|/entries/123/images/2/edit|GET|edit_entry_image_path(entry,image)|
|create|/entries/123/images|POST|entry_images_path(entry)|
|update|/entries/123/images/3|PATCH|entry_image_path(entry,image)|
|destroy|/entries/123/images/4|DELETE|entry_image_path(entry,image)|

### コントローラの実装

- コントローラとビューを生成する。

```sh
$ bin/rails g controller entry_images index new edit
      create  app/controllers/entry_images_controller.rb
      invoke  erb
      create    app/views/entry_images
      create    app/views/entry_images/index.html.erb
      create    app/views/entry_images/new.html.erb
      create    app/views/entry_images/edit.html.erb
```

- EntryImagesControllerのソースを以下のように書き換える。

```rb
class EntryImagesController < ApplicationController
  before_action :login_required

  before_action do
    @entry = current_member.entries.find(params[:entry_id])
  end

  def index
    @images = @entry.images.order(:id)
  end

  def show
    # 編集フォームにリダイレクト
    redirect_to action: "edit"
  end

  def new
    @image = @entry.images.build
  end

  def edit
    @image = @entry.images.find(params[:id])
  end

  def create
    @image = @entry.images.build(image_params)
    if @image.save
      redirect_to [@entry, :images], notice: "画像を作成しました。"
    else
      render "new"
    end
  end

  def update
    @image = @entry.images.find(params[:id])
    @image.assign_attributes(image_params)
    if @image.save
      redirect_to [@entry, :images], notice: "画像を更新しました。"
    else
      render "edit"
    end
  end

  def destroy
    @image = @entry.images.find(params[:id])
    @image.destroy
    redirect_to [@entry, :images], notice: "画像を削除しました。"
  end

  # ストロングパラメータ
  private def image_params
    params.require(:image).permit(
        :new_data,
        :alt_text
    )
  end
end
```

### ブログ記事に添付された画像の一覧

- フッターに、画像のへのリンク`<%= menu_link_to "画像", [entry, :images] %>`を追加する。

```erb:app/views/entries/_footer.html.erb（一部）
    <% if current_member == entry.author %>
      <%= menu_link_to "編集", [:edit, entry] %>
      <%= menu_link_to "画像", [entry, :images] %>
      <%= menu_link_to "削除", entry, method: :delete, data: { confirm: "本当に削除しますか？" } %>
    <% end %>
```

- 画像一覧画面を追加する。
 - `<%= menu_link_to "画像の追加", [:new, @entry, :image] %>`の第2引数は、シンボルとモデルオブジェクトを要素とする配列を指定する。`<%= menu_link_to "画像の追加", new_entry_image_path(@entry, image) %>`と書くこともできる。

```erb:app/views/entry_images/index.html.erb
<% @page_title = "ブログ記事の画像" %>
<h1><%= @page_title %></h1>
<h2><%= @entry.title %></h2>

<ul class="toolbar">
  <%= menu_link_to "ブログ記事に戻る", @entry %>
  <%= menu_link_to "画像の追加", [:new, @entry, :image] %>
</ul>

<% if @images.present? %>
  <table class="list">
    <thead>
      <tr>
        <th>番号</th>
        <th>画像</th>
        <th>代替テキスト</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <% @images.each_with_index do |image, index| %>
        <tr>
          <td><%= index + 1 %></td>
          <td>
            <%= image_tag image.data.variant(resize: "100x>"),
              alt: image.alt_text %>
          </td>
          <td>
            <%= image.alt_text %>
          </td>
          <td>
            <div>
              <%= link_to "編集", edit_entry_image_path(@entry, image) %> |
              <%= link_to "削除", entry_image_path(@entry, image),
                method: :delete, data: { confirm: "本当に削除しますか？" } %>
            </div>
          </td>
        </tr>
      <% end %>
    </tbody>
  </table>
<% else %>
  <p>画像がありません。</p>
<% end %>

```

## 画像追加と画像編集

- 画像追加画面

```erb:app/views/entry_images/new.html.erb
<% @page_title = "ブログ記事への画像追加" %>
<h1><%= @page_title %></h1>
<h2><%= @entry.title %></h2>

<% url = entry_images_path(@entry, @image) %>
<%= form_for @image, as: "image", url: url do |form| %>
  <%= render "form", form: form %>
  <div><%= form.submit %></div>
<% end %>
```

- 画像編集画面

```erb:app/views/entry_images/edit.html.erb
<% @page_title = "ブログ記事の画像編集" %>
<h1><%= @page_title %></h1>
<h2><%= @entry.title %></h2>

<% url = entry_image_path(@entry, @image) %>
<%= form_for @image, as: "image", url: url do |form| %>
  <%= render "form", form: form %>
  <div><%= form.submit %></div>
<% end %>

```

- 部分テンプレート

```erb:app/views/entry_images/_form.html.erb
<%= render "shared/errors", obj: @image %>

<table class="attr">
  <tr>
    <th><%= form.label :new_data %></th>
    <td><%= form.file_field :new_data %></td>
  </tr>
  <tr>
    <th><%= form.label :alt_text %></th>
    <td><%= form.text_field :alt_text, size: 40 %></td>
  </tr>
</table>
```

- 部分テンプレートで使用する日本語ラベルとロケールテキストに追加する。

```yml:config/locales/ja.yml（一部）
ja:
  activerecord:
    attributes:
#（省略）
      entry_image:
        new_data: 画像ファイル
        alt_text: 代替テキスト
```

- ブログ画面から画像リンクをクリックして、画像の追加をクリックすると、画像追加画面が表示された。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/f977e407-3797-06b2-8067-13e79b8bd316.png)

- 画像一覧に登録した画像が表示された。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/4d71155f-b178-a908-7fac-9f1311a10e9b.png)

## 画像の表示

- ブログ記事に画像を表示する。1枚目は本文の前に、2枚目以降は本文の下に表示する。
- views/entries/showを修正する。

```erb:app/views/entries/show.html.erb（一部）
<% @page_title = @entry.title + " - " + @entry.author.name + "さんのブログ" %>
<h1><%= @entry.author.name %>さんのブログ</h1>

<h2><%= @entry.title %></h2>

<%= the_first_image(@entry) %>
<%= simple_format(@entry.body) %>
<%= other_images(@entry) %>
<%= render "footer", entry: @entry %>
```

-　上記で使用したヘルパーメソッド`the_first_image`、`other_images`を実装する。

```rb:app/helpers/entries_helper.rb
module EntriesHelper
  # ブログのトップイメージ
  def the_first_image(entry)
    # 0番目のイメージを取得する
    image = entry.images.order(:id)[0]

    render_entry_image(image) if image
  end

  # ブログの2番目以降のイメージ
  def other_images(entry)
    buffer = "".html_safe
    # 2番目以降すべての画像を取得する。空の配列の場合はnilを返すため、ぼっち演算子&.が必要。
    entry.images.order(:id)[1..-1]&.each do |image|
      buffer << render_entry_image(image)
    end

    buffer
  end

  private def render_entry_image(image)
    content_tag(:div) do
      # 横が530ピクセルよりも大きい場合は、530ピクセルに画像を縮小する。
      image_tag image.data.variant(resize: "530x>"),
                alt: image.alt_text,
                style: "display: block; margin: 0 auto 15px"
    end
  end
end
```

- ブログ画面に複数の画像が表示された。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/56e50885-d887-7922-5d74-f823cb1840b4.png)

# 表示位置の入れ替え

## 準備作業

### Gemパッケージacts_as_listの導入

- モデルオブジェクトの並び順を維持したり、順番を入れ替えたりするにはGemパッケージ`gem 'acts_as_list'`を利用する。

```rb:Gemfile
gem 'acts_as_list'
```

```sh
$ bundle install
```

### モデルクラスにacts_as_listを導入

- `acts_as_list scope: :entry`を追加。`belongs_to`に指定されているシンボル`:entry`を追加する。

```rb:app/models/entry_image.rb（一部）
class EntryImage < ApplicationRecord
  belongs_to :entry
  has_one_attached :data
  acts_as_list scope: :entry  # 追加
```

- ブログ記事の添付画像を全て削除するため、データベースの再構築を行う。

```sh
$ bin/rails db:rebuild
```

### ルーティングの設定

- `do patch :move_higher, :move_lower, on: :member end`を追加する。

```rb:config/routes.rb（一部）
  resources :entries do
    resources :images, controller: "entry_images" do
      patch :move_higher, :move_lower, on: :member
    end
  end
```
- この2つのアクションはいずれもPATCHメソッドを呼び出す。これにより、次の2つのURLパターンが認識される。
    - /entries/123/images/99/move_higher
    - /entries/123/images/99/move_lower
- これらのパターンに沿ったURLパスを生成するメソッド呼び出しは、次のようなものになる。
    - move_higher_entry_image(entry, image)
    - move_lower_entry_image(entry, image)


## 機能の実装

### 表示位置を入れ替えるアクションの実装

- 並び順を制御するために、`order(:id)`を`order(:position)`にする。

```rb:app/controllers/entry_images_controller.rb（一部）
  def index
    @images = @entry.images.order(:position)
  end
```

- 表示位置を上下するアクション`move_higher`と`move_lower`を実装する。
- `redirect_back`メソッドは、アクションの呼び出し元のURLパスにリダイレクションを行う。
    - 呼び出し元はHTTPヘッダーに書かれいているリファラで判定する。
    - `fallback_location`オプションはリファラに書かれていない場合のリダイレクション先を指定する。このオプションは省略できない。

```rb:app/controllers/entry_images_controller.rb（一部）
  def move_higher
    @image = @entry.images.find(params[:id])
    @image.move_higher
    redirect_back fallback_location: [@entry, :images]
  end

  def move_lower
    @image = @entry.images.find(params[:id])
    @image.move_lower
    redirect_back fallback_location: [@entry, :images]
  end
```

### ヘルパーメソッドの変更

- 並び順を制御するために、`order(:id)`となっている部分を`order(:position)`にする。

```rb:app/helpers/entries_helper.rb（一部）
    image = entry.images.order(:position)[0]

    entry.images.order(:position)[1..-1]&.each do |image|
```

### HTMLテンプレートの変更

- 以下のように、削除リンクの横に、「上へ」「下へ」のリンクを追加する。

```erb:app/views/entry_images/index.html.erb（一部）
            <div>
              <%= link_to_if index > 0, "上へ",
                move_higher_entry_image_path(@entry, image),
                method: :patch %> |
              <%= link_to_if index < @images.size - 1, "下へ",
                move_lower_entry_image_path(@entry, image),
                method: :patch %>
            </div>
```

- 一番上の、「下へ」リンクをクリックすると

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/00827dc8-9a76-8c48-54d3-a1e5e907f0c4.png)

- 順番が変わることを確認した。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/54207922-1779-30c5-283c-207dd6580df2.png)

### まとめ

- 複数の画像を登録するには、has_manyのクラスであるentries_imageを追加する。
- acts_as_listというGemパッケージを使うと、リストの並び替えを変更したりできる。


参考
[改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)](https://www.amazon.co.jp/%E6%94%B9%E8%A8%824%E7%89%88-Ruby-Rails-IMPRESS-KISO/dp/429500460X/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=1536666358&sr=1-1)