Ruby on Railsチュートリアル拡張版の第一弾!Railsチュートリアルでは投稿に直接画像を追加するというロジックなのですが、WordPressのように画像は画像ボックスに保存して、保存した画像一覧からサムネイルを選んで記事を保存するのを作りたいと思いまして。今回は作成したメモをRailsチュートリアルみたいにまとめてみました!
本記事で実装する画像アップロード機能のゴール
Railsチュートリアルや他の画像アップローダを紹介するサイトでは、元々作成中のモデルの属性に画像カラムを追加していく方法で説明されています。
本記事では、WordPressのような画像投稿機能を実現するために、画像保存専用のテーブルを作成していくことを目標にします。
そして、最終的には画像一覧から選択して別モデル(Article
モデル)にサムネイルを保存するまでを実装します。
今回作成する機能の具体的なユーザフローは以下のようになっています。
- ユーザが画像投稿フォームにて画像、タイトル、代替テキストを送信
- 画像一覧画面から画像を選択してArticleにサムネイルのURLを保存
今回作成するアップローダの仕様を以下にまとめます。
- 画像保存専用のモデル
Picture
を作成 - ユーザが画像を
CarrierWave
のアップローダで保存 - 画像をアップロードするフォームを作成
- 画像のタイトル、代替テキスト、ファイルを保存
- 画像のバリデーション
- 画像のリサイズ、サムネイル作成
- 画像のサムネイルを選択し、Articleモデルに保存
ロジック部分をメインに実装していくので、みためなどのCSSに本記事ではこだわりません。
見た目部分は雑な作りになっているので、適宜修正してください。
また、メモ書きを無理矢理記事にしているため、説明不足な部分が多いです。
随時、加筆修正していくので、質問・コメント等宜しくお願いします。
本記事のアップローダーを実装するために必要な事前準備
Railsチュートリアルを拡張する形で進めていきます。本記事に必要な事前準備を以下に記します。
- できればRailsチュートリアル第11章まで進める
- 進めたことが無い方は、完成形をcloneしてしまうのが早いかもしれません
- 諸事情により
Micropost
モデルをArticle
モデルとして作成しています
- もしくはUserモデルとArticleモデルを作成する
- UserモデルとArticleモデルさえあれば動くので、手っ取り早く試してみたい方はこちらで
本記事で利用するファイルアップロード用のgem CarrierWave
Ruby on Railsでファイルアップロードを実装するには、Paperclip
とCarrierWave
という2つのgemがよく使われます。
2つのGemの違いは、
Paperclip
は機能がシンプルCarrierWave
は機能が豊富で応用が効く
です。
「rails 画像 アップロード」で調べたところ、CarrierWave
を紹介している記事が多いです。(2016年8月14日現在)
また、CarrierWave
が「Railsチュートリアル」や「パーフェクトRuby on Rails」で紹介されていることから、今回はCarrierWave
を使うことに決めました。
Railsチュートリアルを参考に拡張していくイメージで実装していきます。
CarrierWaveを利用した画像アップロード機能の実装手順
Railsチュートリアル第11章のマイクロポストを真似てピクチャを作成していきます。
ピクチャモデルを作成した後、CarrierWaveを仕様し画像のアップロード機能を実装します。
最後に、mini-magickを利用してアップロードされた画像を選択してサムネイルを記事に設定する機能を作ります。
- Pictureのモデルまわりを実装
- Pictureのバリデーション・デフォルトスコープ
- PictureとUserのリレーション定義
- Pictureのコントローラまわりを実装
- CarrierWaveまわり
- CarrierWaveのUploaderクラスを生成
- Viewにフォームを作成
- バリデーションを設定
- 画像アップロード部分のテスト
- MiniMagickまわり
- ピクチャリストからサムネイル画像を選択
1. Pictureのモデルまわりを実装
Railsチュートリアル第11章のmicropost
モデルを真似しながら、picture
モデルを実装していく
以下のコマンドでPicutreモデルを生成
$ rails generate model Picture url:string title:string alt:string user:references
Pictureのマイグレーションファイルdb/migrate/[timestamp]_create_pictures.rb
にインデックスを付与する記述を追記
class CreatePictures < ActiveRecord::Migration
def change
create_table :pictures do |t|
t.string :title
t.string :alt
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
add_index :pictures, [:user_id, :created_at]
end
end
マイグレーションを叩く
$ rake db:migrate
PicutreモデルのバリデーションとUserモデルとのアソシエーションを定義
Pictureのモデルにuser_id
のバリデーションを追記
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
end
UserのモデルにPictureを複数所有する(hasmany
)関連付け
と
ユーザが削除された時にPictureも削除されるように依存関係を記述する
class User < ActiveRecord::Base
has_many :pictures, dependent: :destroy
.
.
.
end
Picture用のfixtureを追記
orange:
title: "I just ate an orange!"
alt: "orange"
created_at: <%= 10.minutes.ago %>
tau_manifesto:
title: "Check out the @tauday site by @mhartl: http://tauday.com"
alt: "tau_manifesto"
created_at: <%= 3.years.ago %>
cat_video:
title: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
alt: "cat_video"
created_at: <%= 2.hours.ago %>
most_recent:
title: "Writing a short test"
alt: "most_recent"
created_at: <%= Time.zone.now %>
Picutreモデルの有効性テストをtest/models/picture_test.rb
に書く
require 'test_helper'
class PictureTest < ActiveSupport::TestCase
def setup
@user = users(:michael)
@picture = @user.pictures.new(title: "title", alt: "alt")
end
test "画像が有効か" do
assert @picture.valid?
end
test "画像にuser_idが存在するか" do
@picture.user_id = nil
assert_not @picture.valid?
end
test "画像が最新順か" do
assert_equal pictures(:most_recent), Picture.first
end
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 "ユーザが削除されると同時に画像も削除されるか" do
@user.save
@user.pictures.create!(title: "title", alt: "alt")
assert_difference 'Picture.count', -1 do
@user.destroy
end
end
end
2. Pictureのコントローラまわりを実装
users/:user_id/picutres
にアクセスすると、画像の一覧を表示
さらに、ピクチャのCRUDを記述していく
ホーム画面にピクチャ一覧をページネーション表示
Pictureのコントローラをコマンドで生成する
$ rails generate controller Pictures
Userコントローラーにpictures
を記述
...
def pictures
@user = User.find(params[:id])
@pictures = @user.pictures.paginate(page: params[:page])
end
...
1つのPictureを表示するパーシャル
<li id="picture--<%= picture.id %>">
<div class="picture">
</div>
<div class="picture__title"><%= picture.title %></div>
<div class="picture__alt"><%= picture.alt %></div>
</li>
user_pictures
にアクセスするためのルーティング
Rails.application.routes.draw do
...
resources :users do
resources :pictures, only: [:index]
end
resources :users
resources :pictures
...
end
サンプルデータdb/seeds.rb
にPictureを追加
...
users = User.order(:created_at).take(6)
50.times do
title = Faker::Name.title
content = Faker::Lorem.sentence(5)
alt = Faker::Name.title
users.each { |user|
user.articles.create!(title: title, content: content)
user.pictures.create!(title: title, alt: alt)
}
end
シードデータをリセット
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
ユーザのピクチャ一覧画面におけるテスト
$ rails generate integration_test user_pictures
ユーザと関連付けされたピクチャのfixture
test/fixtures/pictures.yml
orange:
title: "I just ate an orange!"
alt: "orange"
created_at: <%= 10.minutes.ago %>
user: michael
tau_manifesto:
title: "Check out the @tauday site by @mhartl: http://tauday.com"
alt: "tau_manifesto"
created_at: <%= 3.years.ago %>
user: michael
cat_video:
title: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
alt: "cat_video"
created_at: <%= 2.hours.ago %>
user: michael
most_recent:
title: "Writing a short test"
alt: "most_recent"
created_at: <%= Time.zone.now %>
user: michael
<% 30.times do |n| %>
picture_<%= n %>:
title: <%= Faker::Lorem.sentence(5) %>
alt: <%= Faker::Name.title %>
created_at: <%= 42.days.ago %>
user: michael
<% end %>
ユーザのピクチャ一覧画面におけるテスト
test/integration/user_pictures_test.rb
require 'test_helper'
class UserPicturesTest < ActionDispatch::IntegrationTest
include ApplicationHelper
def setup
@user = users(:michael)
end
test "ユーザのピクチャ一覧画面を表示" do
get user_pictures_path(@user)
assert_template 'pictures/index'
assert_select 'title', full_title(@user.name)
assert_select 'h1', text: @user.name
assert_select 'h1>img.gravatar'
assert_match @user.pictures.count.to_s, response.body
assert_select 'div.pagination'
@user.pictures.paginate(page: 1).each do |picture|
assert_match picture.title, response.body
assert_match picture.alt, response.body
end
end
end
ピクチャのルーティング設定・アクセス制御
logged_in_user
メソッドをApplicationコントローラに移す
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
Usersコントローラからlogged_in_user
メソッドは消しておく
Pictureコントローラに各アクションを追記
class PicturesController < ApplicationController
before_action :logged_in_user, only: [:show, :new, :create, :edit, :update, :destroy]
def index
@user = User.find(params[:user_id])
@pictures = @user.pictures.paginate(page: params[:page])
end
def show
end
def new
end
def create
end
def edit
end
def update
end
def destroy
end
end
各アクションのルーティングをテスト
require 'test_helper'
class PicturesControllerTest < ActionController::TestCase
def setup
@picture = pictures(:orange)
end
test "ログインしていない時にshowにアクセスしたらリダイレクトされるか" do
get :show, id: @picture
assert_not flash.empty?
assert_redirected_to login_url
end
test "ログインしていない時にnewにアクセスしたらリダイレクトされるか" do
get :new
assert_not flash.empty?
assert_redirected_to login_url
end
test "ログインしていない時にcreateにアクセスしたらリダイレクトされるか" do
assert_no_difference 'Picture.count' do
post :create, picture: { url: "http://localhost:3000/", title: "title", alt: "alt" }
end
assert_redirected_to login_url
end
test "ログインしていない時にeditにアクセスしたらリダイレクトされるか" do
get :edit, id: @picture
assert_not flash.empty?
assert_redirected_to login_url
end
test "ログインしていない時にupdateにアクセスしたらリダイレクトされるか" do
patch :update, id: @picture, picture: { url: "http://localhost:3000/", title: "title", alt: "alt" }
assert_not flash.empty?
assert_redirected_to login_url
end
test "ログインしていない時にdestroyにアクセスしたらリダイレクトされるか" do
assert_no_difference 'Picture.count' do
delete :destroy, id: @picture
end
assert_redirected_to login_url
end
end
ピクチャの作成 new/create
Picturesコントローラのnew
とcreate
アクションを記述
class PicturesController < ApplicationController
before_action :logged_in_user, only: [:show, :new, :create, :edit, :update, :destroy]
...
def new
@picture = current_user.pictures.build if logged_in?
end
def create
@picture = current_user.pictures.build(picture_params)
if @picture.save
flash[:success] = "Picture created!"
redirect_to user_pictures_path(current_user)
else
render 'new'
end
end
...
private
def picture_params
params.require(:picture).permit(:title, :alt)
end
end
Pictureのnewにピクチャ投稿フォームを作成
app/views/pictures/new.html.erb
<div class="row">
<div class="col-md-8">
<section class="picture_form">
<%= render 'shared/picture_form' %>
</section>
</div>
<div class="col-md-4">
<section class="user_info">
<%= render 'shared/user_info' %>
</section>
</div>
</div>
app/views/shared/_picture_form.html.erb
<%= form_for(@picture) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_field :title, placeholder: "title..." %>
</div>
<div class="field">
<%= f.text_field :alt, placeholder: "alt..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
ピクチャの削除 destroy
ピクチャのパーシャルに削除リンクを追加
app/views/pictures/_picture.html.erb
<li id="picture--<%= picture.id %>">
<div class="picture">
<%= image_tag(picture.url, alt: picture.alt) %>
</div>
<div class="picture__title"><%= picture.title %></div>
<div class="picture__alt"><%= picture.alt %></div>
<div class="picture__info">
<% if current_user?(picture.user) %>
<%= link_to "delete", picture, method: :delete, data: { confirm: "You sure?" } %>
<% end %>
</div>
</li>
Picturesコントローラのdestroy
アクション
class PicturesController < ApplicationController
before_action :logged_in_user, only: [:show, :new, :create, :edit, :update, :destroy]
before_action :correct_user, only: :destroy
...
def destroy
@picture.destroy
flash[:success] = "Picture deleted"
redirect_to request.referrer || user_pictures_path(current_user)
end
private
...
def correct_user
@picture = current_user.pictures.find_by(id: params[:id])
redirect_to user_pictures_path(current_user) if @picture.nil?
end
end
fixtures
に別のユーザに所属しているピクチャを追加
...
ants:
title: "Oh, is that what you want? Because that's how you get ants!"
alt: "ants"
created_at: <%= 2.years.ago %>
user: archer
zone:
title: "Danger zone!"
alt: "zone"
created_at: <%= 3.days.ago %>
user: archer
tone:
title: "I'm sorry. Your words made sense, but your sarcastic tone did not."
alt: "tone"
created_at: <%= 10.minutes.ago %>
user: lana
van:
title: "Dude, this van's, like, rolling probable cause."
alt: "van"
created_at: <%= 4.hours.ago %>
user: lana
間違ったユーザによるピクチャの削除をテスト
require 'test_helper'
class PicturesControllerTest < ActionController::TestCase
...
test "間違ったユーザによるピクチャの削除は失敗しリダイレクトされるか" do
@user = users(:michael)
log_in_as(@user)
picture = pictures(:ants)
assert_no_difference 'Picture.count' do
delete :destroy, id: picture
end
assert_redirected_to user_pictures_path(@user)
end
end
ピクチャの更新 edit/update
ピクチャのパーシャルに編集リンクを追加
<li id="picture--<%= picture.id %>">
<div class="picture">
</div>
<div class="picture__title"><%= picture.title %></div>
<div class="picture__alt"><%= picture.alt %></div>
<div class="picture__info">
<% if current_user?(picture.user) %>
<%= link_to "edit", picture, edit_picture_path(picture) %>
<%= link_to "delete", picture, method: :delete, data: { confirm: "You sure?" } %>
<% end %>
</div>
</li>
Picturesコントローラのedit
とupdate
アクションを追記
class PicturesController < ApplicationController
...
def edit
@picture = Picture.find(params[:id])
end
def update
@picture = Picture.find(params[:id])
if @picture.update_attributes(picture_params)
flash[:success] = "Picture updated"
redirect_to user_pictures_path(current_user)
else
render 'edit'
end
end
...
end
Picturesコントローラのテスト
require 'test_helper'
class PicturesControllerTest < ActionController::TestCase
...
test "間違ったユーザによるピクチャの編集editは失敗しリダイレクトされるか" do
@user = users(:michael)
log_in_as(@user)
# 他人のピクチャ
picture = pictures(:ants)
get :edit, id: picture
assert_redirected_to user_pictures_path(@user)
end
test "間違ったユーザによるピクチャの更新updateは失敗しリダイレクトされるか" do
@user = users(:michael)
log_in_as(@user)
# 他人のピクチャ
picture = pictures(:ants)
patch :update, id: picture, picture: { title: "title", alt: "alt" }
assert_redirected_to user_pictures_path(@user)
end
...
end
ピクチャまわりの統合テスト
ピクチャまわりの統合テストを生成
$ rails generate integration_test pictures_interface
ピクチャのUIに対する統合テスト
require 'test_helper'
class PicturesInterfaceTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "ピクチャのインターフェース" do
log_in_as(@user)
get new_picture_path
# 無効な送信は今のところなし(ゆくゆく画像のURLがないとNGにする)
# 有効な送信
title = "This article really ties the room together"
alt = "This article content is very nice"
assert_difference 'Picture.count', 1 do
post pictures_path, picture: { title: title, alt: alt }
end
assert_redirected_to user_pictures_path(@user)
follow_redirect!
assert_match title, response.body
# ピクチャを編集する
assert_select 'a', text: 'edit'
first_picture = @user.pictures.paginate(page: 1).first
get edit_picture_path(first_picture)
assert_template 'pictures/edit'
# 無効な更新は今のところなし(ゆくゆく画像のURLがないとNGにする)
# 有効な更新
new_title = 'new title'
new_alt = 'new alt'
patch picture_path(first_picture), picture: { title: new_title,
alt: new_alt }
assert_not flash.empty?
assert_redirected_to user_pictures_path(@user)
first_picture.reload
assert_equal new_title, first_picture.title
assert_equal new_alt, first_picture.alt
# 投稿の削除
follow_redirect!
assert_select 'a', text: 'delete'
assert_difference 'Picture.count', -1 do
delete picture_path(first_picture)
end
end
end
3. CarrierWaveまわり
画像アップロード用のGemであるCarrierWave
を使って、画像アップロード機能を実装していく
Railsチュートリアル第11章まわりとほぼ同じである
gemにCarrierWaveを追加する
Gemfileにcarrierwave
を追加して、bundle install
コマンドを叩いてgemをインストール
Railsチュートリアルだと、0.10.0
を指定してインストールするが、0.10.0
にはバグがある
今回は、0.11.0
をインストール
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
gem 'faker', '1.4.2'
gem 'carrierwave', '0.11.0'
...
CarrierWaveのUploaderクラスを生成する
CarrierWaveのジェネレーターでPictureアップローダーを作成する
$ rails generate uploader Picture
Pictureモデルにfile
カラムを追加するためマイグレーション生成
$ rails generate migration add_file_to_pictures file:string
$ bundle exec rake db:migrate
Picture
モデルにfile
カラムを追加し、Pictureアップローダを設定
class Picture < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
mount_uploader :file, PictureUploader
validates :user_id, presence: true
end
ここで一旦Rail serverを再起動
Viewに画像アップロードフォームを追記
ピクチャフォームに画像アップローダーを追加
app/views/shared/_picture_form.html.erb
<%= form_for(@picture, html: { multipart: true }) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<span class="picture">
<%= f.file_field :file %>
</span>
<div class="field">
<%= f.text_field :title, placeholder: "title..." %>
</div>
<div class="field">
<%= f.text_field :alt, placeholder: "alt..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
file
を許可するparamsのリストに追加
class PicturesController < ApplicationController
...
private
def picture_params
params.require(:picture).permit(:title, :alt, :file)
end
def correct_user
@picture = current_user.pictures.find_by(id: params[:id])
redirect_to user_pictures_path(current_user) if @picture.nil?
end
end
ピクチャのパーシャルに画像を追加
app/views/pictures/_picture.html.erb
<li id="picture--<%= picture.id %>">
<div class="picture__file">
<%= image_tag picture.file.url if picture.file? %>:
</div>
...
</li>
アップローダのバリデーションを設定する
Carrierwaveはデフォルトでは、アップロードされた画像に対する制限がないなどいくつかの欠点がある。
欠点を直すためにバリデーションを設定していく必要がある。
画像フォーマットのバリデーション
app/uploaders/picture_uploader.rb
に追記
class PictureUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# include CarrierWave::RMagick
# include CarrierWave::MiniMagick
# 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
# アップロード可能な拡張子のリスト
def extension_white_list
%w(jpg jpeg gif png)
end
...
end
画像サイズに対するバリデーション
ファイルサイズに対するバリデーションはデフォルトのRailsにはない
独自のバリデーションをPictureモデルに定義する
class Picture < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
mount_uploader :file, PictureUploader
validates :user_id, presence: true
validate :file_size
private
# アップロード画像のサイズを検証する
def file_size
if file.size > 5.megabytes
errors.add(:picture, "should be less than 5MB")
end
end
end
フロント側でのバリデーション制御
大きすぎるファイルのバリデーションは、バックエンドだけでなくフロントエンドでも行う
そうすることで、長過ぎるアップロード時間を防いだり、サーバーの負荷を軽減できる
ファイルサイズとファイルの拡張子をjQueryでチェックする
<%= form_for(@picture, html: { multipart: true }) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="picture">
<%= f.file_field :file, accept: 'image/jpeg,image/git,image/png' %>
</div>
<div class="field">
<%= f.text_field :title, placeholder: "title..." %>
</div>
<div class="field">
<%= f.text_field :alt, placeholder: "alt..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
<script type="text/javascript">
$('#picture_file').bind('change', function() {
var size_in_megabytes = this.files[0].size/1024/1024;
if (size_in_megabytes > 1) {
alert('Maximum file size is 5MB. Please choose a smaller file.');
}
});
</script>
.gitignore
ファイルに画像用ディレクトリを追記
...
# Ignore uploaded test images.
/public/uploads
画像アップロード部分のテスト
今回のテストの要件を以下にまとめます。
- 画像が添付されていない場合は、ピクチャを保存しない
- タイトルや代替テキストは空でもOK
ピクチャモデルのテスト
まず、サンプル画像をfixtureディレクトリに追加する
cp app/assets/images/rails.png test/fixtures/
画像アップロードがなかった場合バリデーションエラーとするロジックを追記してテスト
ここのテストが通らず多くの時間を費やしてしまいました。
何が起きていたかといいますと、
ピクチャモデルのテストに
test "ピクチャにfileが存在するか" do
@picture.file = nil
assert_not @picture.valid?
end
と書いていたのですが、これだとなぜか@picture.file
をnil
で上書きできていませんでした。
そこで調べてみると、以下の本家ドキュメントで解決策を見つけることができました。
If you want to remove the file manually, you can call remove_avatar!
とのこと
これを参考に、
test "ピクチャにfileが存在するか" do
@picture.remove_file!
assert_not @picture.valid?
end
としたら、テストが通りました。
もう一つなかなか解決できなかったのが、fixture_file_upload
関数がモデルのテストだと動かないことです。
Railsチュートリアル第11章の画像アップローダのテストでfixture_file_upload
メソッドが使われています。同じやり方でモデルでもテストできると思ったのですが、以下のエラーが発生。
NoMethodError: undefined method `fixture_file_upload' for #<PictureTest:0x007fc2f135f960>
調べてみると、統合テストはActionDipatch::TestProcess
を継承しているから、fixture_file_upload
メソッドが使えることが判明しました。したがって、モデルでfixture_file_upload
メソッドを使うためには、以下のコードをfixture_file_upload
メソッドを呼ぶ前に追記する必要があります。
extend ActionDispatch::TestProcess
以上の2点を踏まえて、追記したピクチャモデルのテストコードは以下になります。
require 'test_helper'
class PictureTest < ActiveSupport::TestCase
def setup
@user = users(:michael)
extend ActionDispatch::TestProcess
file = fixture_file_upload('rails.png', 'image/png')
@picture = @user.pictures.new(title: "title", alt: "alt", file: file)
end
...
test "ピクチャにuser_idが存在するか" do
@picture.user_id = nil
assert_not @picture.valid?
end
test "ピクチャにfileが存在するか" do
@picture.remove_file!
assert_not @picture.valid?
end
...
end
また、ピクチャモデルのバリデーションを変更したので、今までOKだったユーザモデルのバリデーションが通らなくなったりします。その場合、適宜テストが通るように修正してください。
ピクチャの統合テストに画像アップロード部分を追記する
追記したピクチャの統合テストは以下になります。
require 'test_helper'
class PicturesInterfaceTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "ピクチャのインターフェース" do
log_in_as(@user)
get new_picture_path
assert_select 'input[type=file]'
# 無効な送信
title = "This article really ties the room together"
alt = "This article content is very nice"
assert_no_difference 'Picture.count' do
post pictures_path, picture: { title: title, alt: alt }
end
assert_select 'div#error_explanation'
# 有効な送信
title = "This article really ties the room together"
alt = "This article content is very nice"
file = fixture_file_upload('test/fixtures/rails.png', 'image/png')
assert_difference 'Picture.count', 1 do
post pictures_path, picture: { file: file, title: title, alt: alt }
end
assert_redirected_to user_pictures_path(@user)
picture = assigns(:picture)
assert picture.file?
follow_redirect!
assert_match title, response.body
# ピクチャを編集する
assert_select 'a', text: 'edit'
first_picture = @user.pictures.paginate(page: 1).first
get edit_picture_path(first_picture)
assert_template 'pictures/edit'
# 無効な更新は今のところなし(ゆくゆく画像のURLがないとNGにする)
# 有効な更新
new_title = 'new title'
new_alt = 'new alt'
patch picture_path(first_picture), picture: { title: new_title,
alt: new_alt }
assert_not flash.empty?
assert_redirected_to user_pictures_path(@user)
first_picture.reload
assert_equal new_title, first_picture.title
assert_equal new_alt, first_picture.alt
# ピクチャの削除
follow_redirect!
assert_select 'a', text: 'delete'
assert_difference 'Picture.count', -1 do
delete picture_path(first_picture)
end
end
end
4. MiniMagickまわり
CarrierWave
では画像をアップロードの部分までしか実装できません。
MiniMagick
というGemを使って、画像をアップロードした際に自動的にサムネイル画像を生成する機能を実装していきます
画像のリサイズ
前準備として、ImageMagickという画像を操作するためのパッケージが必要
MacでImageMagickをインストール
Homebrewを使うのが簡単
brew install imagemagick
UbuntuなどのDebian系OSでImageMagickをインストール
$ sudo apt-get update
$ sudo apt-get install imagemagick --fix-missing
MiniMagickのインストール
MiniMagick
というGemを使って、CarrierWaveからImageMagickを使えるようにする
Gemfileにmini_magick
を追加して、bundle install
コマンドを叩いてgemをインストール
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
gem 'faker', '1.4.2'
gem 'carrierwave', '0.11.0'
gem 'mini_magick', '3.8.0'
...
CarrierWaveのアップローダにリサイズを設定
アップロードすると最大1280×720にリサイズされる
リサイズに加え、100×100のサムネイルも生成する
# encoding: utf-8
class PictureUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# include CarrierWave::RMagick
include CarrierWave::MiniMagick
# 画像のサイズを変更
process :resize_to_limit => [1280, 720]
# サムネイルの生成
version :thumb do
process :reseize_to_fill => [100, 100]
end
...
end
ピクチャのパーシャルをサムネイルに変更
app/views/picutres/_picture.html.erb
を変更
<li id="picture--<%= picture.id %>">
<div class="picture__file">
<%= image_tag picture.file.thumb.url if picture.file? %>
</div>
<div class="picture__title"><%= picture.title %></div>
<div class="picture__alt"><%= picture.alt %></div>
<div class="picture__info">
<% if current_user?(picture.user) %>
<%= link_to "edit", edit_picture_path(picture) %>
<%= link_to "delete", picture, method: :delete, data: { confirm: "You sure?" } %>
<% end %>
</div>
</li>
TODO MiniMagickまわりのテスト
MiniMagickまわりのテストを書いていません。
今後、追加していきたいと思います。
5. ピクチャリストからサムネイル画像を選択
サムネイル画像を選択ボタンを押すと、画像リストが出現
will_paginate
を使い、動的に「もっと見る」を実装
サムネイル画像を選択すると記事のpicture_id
にセットされるように
記事の作成と編集フォームにサムネイル選択ボタンを追加
記事の作成フォームapp/view/articles/new.html.erb
と
記事の編集フォームapp/view/articles/edit.html.erb
のサイドバーに以下を追記。
...
<section class="user_info">
<%= render 'shared/user_info' %>
</section>
<section class="user_pictures">
<%= render 'shared/user_pictures' %>
</section>
...
app/views/shared/_user_pictures.html.erb
を作成してサムネイル選択ボタンを追記
<%= link_to "Select Thumbnail Picture", user_pictures_path(current_user), page: 1,
remote: true, id: 'userPictures__moreLink', onclick: "nowLoading();" %>
<script type="text/javascript">
function nowLoading() {
$("#userPictures__moreLink").replaceWith('<a href="#" id="userPictures__moreLink--nowLoading">Now Loading...</a>');
};
</script>
サムネイル一覧を取得するためのAjax通信部分
Pictureコントローラのindexに以下を追加
class PicturesController < ApplicationController
before_action :logged_in_user, only: [:show, :new, :create, :edit, :update, :destroy]
before_action :correct_user, only: [:edit, :update, :destroy]
def index
@user = User.find(params[:user_id])
@pictures = @user.pictures.paginate(page: params[:page])
end
...
Ajaxでpictures#indexにアクセスしたときに返すHTMLをapp/views/picutures/index.js.erb
に書く
$(".user_pictures").append("<%= escape_javascript(render('shared/user_pictures')) %>");
$("#userPictures__moreLink--nowLoading").remove();
サムネイル一覧を表示する部分をapp/views/shared/_user_pictures.html.erb
に追記
<% if @pictures %>
<% @pictures.each do |picture| %>
<div class="pictureFileThumb">
<%= image_tag picture.file.thumb.url, data: { id: picture.id } if picture.file? %>
</div>
<% end %>
<% if @pictures.next_page %>
<%= render 'shared/user_pictures_more' %>
<% end %>
<% else %>
<%= link_to "Select Thumbnail Picture", user_pictures_path(current_user), page: 1,
remote: true, id: 'userPictures__moreLink', onclick: "nowLoading();" %>
<% end %>
...
サムネイルをさらに取得するためのボタンをapp/views/shared/_user_pitcures_more.html.erb
に追記
<%= link_to "More...", user_pictures_path(current_user, page: @pictures.next_page),
remote: true, id: 'userPictures__moreLink', onclick: "nowLoading();" %>
記事にサムネイルカラムを追加
$ rails generate migration add_thumb_to_articles thumb:text
$ rake db:migrate
サムネイルとクリックすると記事フォームのthumbにpicture.file.thumb.urlがセットされるように
app/views/shared/_article_form.html.erb
のフォームにthumb
を追加する。
<%= form_for(@article) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_field :title, placeholder: "title..." %>
</div>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new article..." %>
</div>
<div class="field">
<%= f.text_field :thumb, placeholder: "url for Thumbnail..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
app/views/article/new.html.erb
とapp/views/article/edit.html.erb
に以下のJavaScriptを追記
...
<section class="user_pictures">
<%= render 'shared/user_pictures' %>
</section>
<script type="text/javascript">
function nowLoading() {
$("#userPictures__moreLink").replaceWith('<a href="#" id="userPictures__moreLink--nowLoading">Now Loading...</a>');
};
$(document).on('click', '.pictureFileThumb img', function() {
$('.pictureFileThumb--selected').each(function() {
$(this).removeClass('pictureFileThumb--selected');
});
$(this).addClass('pictureFileThumb--selected');
console.log($(this).attr('src'));
var thumb = $(this).attr("src");
$('#article_thumb').val(thumb);
});
</script>
...
app/controller/articles_controller.rb
のarticle_params
にthumb
を追加
...
private
def article_params
params.require(:article).permit(:title, :content, :thumb)
end
...
Viewのもろもろを変更
ユーザの記事一覧画面にサムネイルを表示させ、タイトルをクリックすると記事ページヘ
<li id="article--<%= article.id %>">
<div class="article__thumb">
<%= image_tag article.thumb %>
</div>
<div class="article__title">
<%= link_to article.title, article_path(article) %>
</div>
...
記事のshow画面でタイトルとコンテンツを表示
<% provide(:title, 'Article show') %>
<h1>Article show</h1>
<h2><%= @article.title %></h2>
<div>
<%= @article.content %>
</div>
記事周りのテストを追加
記事モデルのバリデーションを追加しテスト
サムネイルを必須項目にする
テストから書く
require 'test_helper'
class ArticleTest < ActiveSupport::TestCase
def setup
@user = users(:michael)
@article = @user.articles.build(title: "title", content: "content", thumb: "thumb")
end
...
test "記事にthumbが存在するか" do
@article.thumb = " "
assert_not @article.valid?
end
...
class Article < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
validates :user_id, presence: true
validates :title, presence: true
validates :content, presence: true
validates :thumb, presence: true
end
thumbを必須項目にすると今までのテストでエラーが出るのでそれは適当に修正していく
本記事のまとめ
本記事では、WordPressのような画像投稿機能を実現する方法をRailsチュートリアルのようにまとめてみました。
今回作成した具体的な機能は以下のようになっています。
- 画像保存専用のモデル
Picture
を作成 - ユーザが画像を
CarrierWave
のアップローダで保存 - 画像をアップロードするフォームを作成
- 画像のタイトル、代替テキスト、ファイルを保存
- 画像のバリデーション
- 画像のリサイズ、サムネイル作成
- 画像のサムネイルを選択し、ArticleモデルにURLを保存
ロジック部分をメインに実装していくので、みためなどのCSSをほとんど書きませんでした。
見た目部分は雑な作りになっているので、適宜修正してください。
また、最初にも書きましたがメモ書きを無理矢理記事にしているため、説明不足な部分が多いです。
随時、加筆修正していくので、質問・コメント等宜しくお願いします。
本記事の参考サイト
基本的にRailsチュートリアルの第11章を参考にしました。
第11章ユーザーのマイクロポスト | Rails チュートリアル
CarrierWave関連の参考
RailsのファイルをアップロードするgemのCarrierWaveのインストール方法 - Rails Webook
CarrierWaveを利用した画像ファイルのアップロード - blog.beaglesoft.net
mini_magick関連の参考
Carrierwaveで画像をリサイズする - 49hack
CarrierWave + RMagick 画像のリサイズをまとめてみました - 麺処 まつば
RMagick で正方形のサムネイルを作成する - Qiita