前 基礎Ruby on Rails #22 Chapter13 ファイルのアップロード
次 基礎Ruby on Rails #24 Chapter13 クラウドスレレージサービス(Amazon S3)の利用
ブログ画像のアップロード
EntryImageクラスの作成
- Active Storageにはモデルと複数個のファイルを結びつけるためのクラスメソッド
has_many_attached
が用意されているが、ファイルの一部を差し替えたり、順番を入れ替えたりできないという制約がある。 - そこで、個々のブログ写真を添付するためのモデル
EntryImage
を作成する。(1つのEntryレコードに、複数のEntryImageレコードがぶら下がる)
$ bin/rails g model entry_image
invoke active_record
create db/migrate/20181020120055_create_entry_images.rb
create app/models/entry_image.rb
- 作成されたマイグレーションスクリプトを以下のように3項目追加する。
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
- マイグレーションを実行する。
$ bin/rails db:migrate
== 20181020120055 CreateEntryImages: migrating ================================
-- create_table(:entry_images)
-> 0.0021s
== 20181020120055 CreateEntryImages: migrated (0.0024s) =======================
- EntryImagesと、プロフィール画像が登録できるMemberモデルと比較すると以下の部分が異なる。
- 削除のチェックボックスとその項目がない→レコードごと削除するので不要
- 空禁止のバリデーションがある(createのみ)→プロフィール画像は空も可で、対してこちらは不要なときはレコードごと削除するため空で登録することがない。createのみなのは代替テキストのみ変更する場合があるため。
- 削除処理をコントローラーのdestroyに書き、こちらに書かないため保存するのがシンプル。
- EntryImageモデルを以下のように書き換える。
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でブログ記事の写真一覧が取得できる。
app/models/entry.rb(一部)
has_many :images, class_name: "EntryImage"
画像のアップロードと削除
ルーティングの設定
- ブログ画像は1つのブログに結び付けられるので、ネストしたルーティングとして登録する。
- 以下のように、controllerオプションを用いると、リソース名とコントローラ名の間を自由に関連付けることができる。
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) |
コントローラの実装
- コントローラとビューを生成する。
$ 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のソースを以下のように書き換える。
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] %>
を追加する。
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) %>
と書くこともできる。
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 %>
画像追加と画像編集
- 画像追加画面
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 %>
- 画像編集画面
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 %>
- 部分テンプレート
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>
- 部分テンプレートで使用する日本語ラベルとロケールテキストに追加する。
config/locales/ja.yml(一部)
ja:
activerecord:
attributes:
#(省略)
entry_image:
new_data: 画像ファイル
alt_text: 代替テキスト
- ブログ画面から画像リンクをクリックして、画像の追加をクリックすると、画像追加画面が表示された。
- 画像一覧に登録した画像が表示された。
画像の表示
- ブログ記事に画像を表示する。1枚目は本文の前に、2枚目以降は本文の下に表示する。
- views/entries/showを修正する。
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
を実装する。
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
- ブログ画面に複数の画像が表示された。
表示位置の入れ替え
準備作業
Gemパッケージacts_as_listの導入
- モデルオブジェクトの並び順を維持したり、順番を入れ替えたりするにはGemパッケージ
gem 'acts_as_list'
を利用する。
Gemfile
gem 'acts_as_list'
$ bundle install
モデルクラスにacts_as_listを導入
-
acts_as_list scope: :entry
を追加。belongs_to
に指定されているシンボル:entry
を追加する。
app/models/entry_image.rb(一部)
class EntryImage < ApplicationRecord
belongs_to :entry
has_one_attached :data
acts_as_list scope: :entry # 追加
- ブログ記事の添付画像を全て削除するため、データベースの再構築を行う。
$ bin/rails db:rebuild
ルーティングの設定
-
do patch :move_higher, :move_lower, on: :member end
を追加する。
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)
にする。
app/controllers/entry_images_controller.rb(一部)
def index
@images = @entry.images.order(:position)
end
- 表示位置を上下するアクション
move_higher
とmove_lower
を実装する。 -
redirect_back
メソッドは、アクションの呼び出し元のURLパスにリダイレクションを行う。- 呼び出し元はHTTPヘッダーに書かれいているリファラで判定する。
-
fallback_location
オプションはリファラに書かれていない場合のリダイレクション先を指定する。このオプションは省略できない。
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)
にする。
app/helpers/entries_helper.rb(一部)
image = entry.images.order(:position)[0]
entry.images.order(:position)[1..-1]&.each do |image|
HTMLテンプレートの変更
- 以下のように、削除リンクの横に、「上へ」「下へ」のリンクを追加する。
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>
- 一番上の、「下へ」リンクをクリックすると
- 順番が変わることを確認した。
まとめ
- 複数の画像を登録するには、has_manyのクラスであるentries_imageを追加する。
- acts_as_listというGemパッケージを使うと、リストの並び替えを変更したりできる。