Micropostモデルの作成
投稿はユーザと紐付ける必要があるので、
必要なカラムは、content:text と user_id:integerである。
では、それを作るコマンドを見ていこう
$ rails generate model Micropost content:text user:references
user:referencesをすることで、user_idとidの結びつきを指定することができるらしい。
ショートカットの方法の一つ
そうすると、micropostモデルファイルは、、
class Micropost < ApplicationRecord
belongs_to :user
end
できたマイグレーションファイルにindexを追加する
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
indexは索引や目次のようなもので、この基準でリソースを参照したい時、よりスピーディーに参照できるようにするもの。
何がやりたいかというと、
私たちがDBにアクセスするときは、user_idといつその投稿が作られたかという基準で頻繁にアクセスしますよーということをDBに伝えて、それに適した設定になるようにしている。
find_byする時に検索するスピードをあげるためのスペースを作りますよーってことらしい。
次は、Userモデルに「たくさんのmicropostsを持てますよー」ということを伝えなければいけない。
なので、
class User < ApplicationRecord
has_many :microposts
.
.
.
end
となる。これで1対多の関係ができた。
Micropostモデルの実装
モデルのバリデーションに関してはTDDで開発していく。
また、関連付けしたことによって、以下のような便利なメソッドが使えるようになる。
Micropost.find_by(user_id: user_id)
これが
user.microposts
Micropost.new(user_id: user_id)
これが、
user.microposts.build
Micropost.create(user_id: user_id)
これが
user.microposts.create
Micropost.find_by(user_id: ~)
これが
user.microposts.find_by(~~)
というように書き換えることができる。
micropostモデルのテスト
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
このテストは本来バリデーションで検知して失敗するはずなのに、通ってしまう。
そのため、バリデーションを設定していこう
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
end
さらにmicropostモデルのバリデーションのテストを進める。
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
これでは、空のコンテンツと141文字以上のコンテンツがvalidになってしまうので、バリデーションを設定する。
class Micropost < ApplicationRecord
belongs_to :user
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end
これでテストは通る。
実はテストで、
def setup
@user = users(:michael)
# このコードは慣習的に正しくない
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
end
このようなコードがあったが、これは慣習的に正しくない。
関連付けができているので、
def setup
@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")
end
というように書くのが一般的
この書き方を使うと、
current_user.microposts.buildというようの書くこともできる。
マイクロポストを改良する
1.投稿の表示順序を降順にする。
2.Userが破棄されたら関連する投稿も全て破棄されるようにする。
では、これらの機能を実装していこう。
デフォルトスコープをTDDで実装していく
まずは作られた日時が違う投稿をtest/fixtures/microposts.ymlに定義する
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
tau_manifesto:
content: "Check out the @tauday site by @mhartl: http://tauday.com"
created_at: <%= 3.years.ago %>
cat_video:
content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
created_at: <%= 2.hours.ago %>
most_recent:
content: "Writing a short test"
created_at: <%= Time.zone.now %>
これてテスト用のサンプルデータができた。
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
.
.
.
test "order should be most recent first" do
assert_equal microposts(:most_recent), Micropost.first
end
end
このテストは、デフォルトスコープが効いてるなら、一番最新の投稿とMicropostの1番目に参照される投稿は同じだよねー?ってテスト。
この状態ではデフォルトスコープ未実装なので失敗する。
では、デフォルトスコープを実装しよう
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
これでテストが通る。
多分、デフォルトスコープはクエリが呼ばれた時にブロック内に書いたコードがクエリに反映されて、SQL文が発行されるという仕組み?
次は、ユーザーが破棄されたら、それに関連する投稿も破棄されるようにする。
これは簡単で、has_manyメソッドにオプションを渡してあげることで解決する。
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
.
.
.
end
これにより、ユーザーが破棄されたら投稿も破棄される。
dependent: :destroyのテスト をする。
このオプションはuserモデルのファイルに書くので、テストも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
ここで注目したいのが、create!である。
投稿を作れてないまま次のテストに行っても意味がないので、もし投稿が作れなかったときは例外を出すようにする。
この時点では、micropostsコントローラやview、ルーティングも設定してないので、テストが通る。
有効化してなかったら投稿作れないなどに規制がないため。
マイクロポストを表示する
今回注意すべき点は、micropostsを表示するのはUserリソースのviewだということ。
まずは、まだ実装していなかったコントローラを作ろう
$ rails generate controller Microposts
ここではアクションをしていない。
先ほど書いたように、micropostsはUserリソースのviewで使うため、ただ、createアクションやdestroyアクションは今後書き足していく必要がある。
また、micropostsを表示する時のコードを見ていこう
<ol class="microposts">
<%= render @microposts %>
</ol>
これはUserリソースのindexアクションでやったのと同じだ。
render @リソース名をすると、
_リソース名の単数形 のパーシャルを展開することができる。
これを今回も利用していく。
では、パーシャルを先に作っていこう
<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>
link_toの第二引数に入っているmicropost.userを見ていこう。
これはuser_path(micropost.user)の略だよね?
これは関連付けが終わっているからこそできる技。
また、time_ago_in_wordsメソッドに引数を渡すと、
今から逆算して「何日まえに作成されたよー」という情報を参照することができる。
次に、@micropostsはユーザーのプロフィールページで使うため、showアクションの中で参照できるようにする。
class UsersController < ApplicationController
.
.
.
def show
@user = User.find(params[:id])
@microposts = @user.microposts.paginate(page: params[:page])
end
.
.
.
end
ここで注目すべきは、最初からpagenateをかけているという点。
params[:page]は自動生成されたような気がする。
次に実際にshowアクションのviewにmicropostsが表示されるようにしていく。
<% 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>
if @user.microposts.any? というコードに注目。
1つも投稿してないというケースがあるので、
.any?メソッドを使って、投稿がある場合という条件を与えている。
これはif errors.any?の時と同じである。
micropostsが存在する時に、_micropostパーシャルを展開するような感じだ。
また、<%= will_paginate @microposts %>には要注意だ。
本来will_pagenateだけだと、書かれているコンテキストをpagenationするのでUserだと勘違いを起こしてしまう。
なので、それを防ぐために、引数に@micropostsを指定して、投稿をページネーションするようにしている。
ただ、このままではユーザーのプロフィールページに投稿が表示されないので、
マイクロポストのサンプルデータを作る。
db/seeds.rbにサンプルデータを定義しよう
.
.
users = User.order(:created_at).take(6)
50.times do
content = Faker::Lorem.sentence(5)
users.each { |user| user.microposts.create!(content: content) }
end
usersを定義する際にtakeメソッドが使われている。
これは作られたユーザーの最初の6人を意味する。
contentを作る際にLorem.sentence(5)というメソッドが使われている。
そしたら、dbをリセットしてサンプルを作り直す
$ rails db:migrate:reset
$ rails db:seed
表示されたが、デザインが悪いので、cssを設定する。
.
.
/* microposts */
.microposts {
list-style: none;
padding: 0;
li {
padding: 10px 0;
border-top: 1px solid #e8e8e8;
}
.user {
margin-top: 5em;
padding-top: 0;
}
.content {
display: block;
margin-left: 60px;
img {
display: block;
padding: 5px 0;
}
}
.timestamp {
color: $gray-light;
display: block;
margin-left: 60px;
}
.gravatar {
float: left;
margin-right: 10px;
margin-top: 5px;
}
}
aside {
textarea {
height: 100px;
margin-bottom: 5px;
}
}
span.picture {
margin-top: 10px;
input {
border: 0;
}
}
これでデザインが整う
プロフィール画面のマイクロポストをテストする。
$ rails generate integration_test users_profile
統合テストを行う。
次にテスト用のマイクロポストがないと意味ないので、定義する
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 %>
ここで注意すべき点は、ユーザーとの関連付けだ。
user: michael
とすることで、users.ymlのユーザーと関連づけることができる。
これで、統合テストを書いていく。
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
ここで注目すべき点は、
include ApplicationHelper
と
assert_select 'title', full_title(@user.name)
だ。
applicationhelperにはタイトルを決めるメソッドを定義した。ただ、テストではこれは使えないのでincludeしている。
このままでテストは通る
マイクロポストを操作する。
投稿したり表示したりできるようにする。
今回は、投稿用のform_forはuserリソースのshowアクションのviewの中にお邪魔させてもらうので、
newアクションやeditアクションは不要。
必要なのは、createアクションとdestroyアクションということになる。
まずはルーティングを用意する。
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
マイクロポストのアクセス制御
rails g controllerで作られたテストファイルにテストを書いていく。
具体的には適切なユーザーじゃないと使えませんよーというもの。
てか、コントローラーテスト自体そういう目的なんじゃ?
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
これはログインしてないと、createアクションもdestroyアクションも使えませんよーってこと。
この状態ではテストは失敗する。
これの解決法は、
まずlogged_in_user
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
これをuserコントローラに定義してbefore_actionに指定していたのを思い出そう。
これをapplicationコントローラに移せば、micropostsコントローラでもbefore_actionとして使えるんじゃない?って話。
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
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
def create
end
def destroy
end
end
これでテストが通る。
マイクロポストを作成する
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
ストロングパラメーターのところは、ユーザーと投稿が関連づけられてるからuser_idを引っ張ってくる必要がないということ?userと関連づけられてるから、勝手にuserのidがuser_idに代入されるということ?
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>
<% 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 %>
if elseでログインしてるかどうかで表示する内容を変えている
また、
<%= render 'shared/user_info' %>
<%= render 'shared/micropost_form' %>
のように、先にパーシャルを用意しておいて、それを展開するようにする
まずはユーザー情報を表示するパーシャルを作っていこう
<%= 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>
pluralizeは投稿が2つ以上ならmicropostsになるというメソッド。
次は、投稿フォームを作っていこう
<%= 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 %>
注目すべきは object: f.object である。
そもそも、エラーメッセージのパーシャルは@userに起きたエラーを表示するものになっていた。
なので、micropostにエラーが起きた時に本来なら表示されないのである。
なので、@userでも@micopostでもどちらでもエラーメッセージが出るようにパーシャルの方をリファクタリングしてやろうという話になる。
また、<%= render 'shared/error_messages', object: f.object %>
にある通り、オプションでobject: f.objectと書くとそのオプションがレンダリングされた先でも使えるようになるという特性がある。
そして、f.objectというのはform_forに渡されたオブジェクトという意味なので、userフォームなら@userが、投稿フォームなら@micropostが入るようになる。
これを踏まえてパーシャルを書き換える
<% 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 %>
ただ、このままだとテストは失敗してしまう。
理由は以下
次に、
<%= render 'shared/error_messages', object: f.object %>
投稿フォームの場合は、オプションを指定したが、他にもerror_messageパーシャルをレンダリングしているページがあるので、そこでもオプションを渡すようにする。そうじゃないと、適切にエラーメッセージが表示されない。
具体的に変える必要があるのは
user登録フォーム、編集フォーム、パスワード再設定フォームである。
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails">change</a>
</div>
</div>
</div>
<% provide(:title, 'Reset password') %>
<h1>Password reset</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= hidden_field_tag :email, @user.email %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Update password", class: "btn btn-primary" %>
<% end %>
</div>
</div>
また、投稿フォームのパーシャルで@micropostを使っているが、それがまだ定義されてないので、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
これでテストが通るようになる。
form_forの引数って?
そもそも、form_forの引数はモデルクラスのインスタンスを指定する。
で、新しく作られていたらcreateアクション、編集されていたらupdateアクションに飛ばすというもの。
フィードを作っていく
マイページの横に自分の投稿と自分がフォローしている投稿が表示されるようにしたい。
それを作っていこう。
まずはfeedメソッドを定義する。定義する場所は、全てのユーザーにfeedを用意したいということから、modelに定義する。
class User < ApplicationRecord
.
.
.
# 試作feedの定義
# 完全な実装は次章の「ユーザーをフォローする」を参照
def feed
Micropost.where("user_id = ?", id)
end
private
.
.
.
end
Micropost.where("user_id = ?", id)に関して、
上の疑問符があることで、SQLクエリに代入する前にidがエスケープされるため、SQLインジェクション (SQL Injection) と呼ばれる深刻なセキュリティホールを避けることができます。この場合のid属性は単なる整数 (すなわちself.idはユーザーのid) であるため危険はありませんが、SQL文に変数を代入する場合は常にエスケープする習慣をぜひ身につけてください。
らしいです。
次にhomeアクションにfeedのインスタンス変数を追加する。
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
次に、フィードを表示するためのパーシャルを作っていく。
<% if @feed_items.any? %>
<ol class="microposts">
<%= render @feed_items %>
</ol>
<%= will_paginate @feed_items %>
<% end %>
ここで注意すべきは、feed_itemsはmicropostの集合体ということ。
このとき、@feed_itemsの各要素がMicropostクラスを持っていたため、RailsはMicropostのパーシャルを呼び出すことができました。このように、Railsは対応する名前のパーシャルを、渡されたリソースのディレクトリ内から探しにいくことができます。
なので,_micropostパーシャルが展開される。
そして、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 %>
ただ、この状態だと問題がある。
投稿に失敗した時にエラーになってしまうのだ。
なぜなら投稿に失敗すると、@feed_itemsがnilになるので、それに対してany?メソッドを呼び出すことはできないからだ。
なので、失敗したケースを考えて、micropostsコントローラのcreateアクションに次のように書き加える必要がある。
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
@feed_items = []を足すことで、もし失敗してもインスタンス変数が存在するから大丈夫。
マイクロポストを削除する
その投稿が自分と紐づいているものならdeleteリンクを表示して削除できるようにする。
まずはそのリンクをマイクロポストパーシャルに追加する。
<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>
次に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
def correct_userメソッドに注意。
これはdeleteリクエストを送った時 microposts/:idというurlになることから。
current_userが作ったmicropostsの中にid == params[:id]の投稿がありますか?という条件を作り出している。
また、redirect_to request.referrer || root_urlに注目しよう
ここではrequest.referrerというメソッドを使っています12。このメソッドはフレンドリーフォワーディングのrequest.url変数 (10.2.3) と似ていて、一つ前のURLを返します (今回の場合、Homeページになります)13。このため、マイクロポストがHomeページから削除された場合でもプロフィールページから削除された場合でも、request.referrerを使うことでDELETEリクエストが発行されたページに戻すことができるので、非常に便利です。ちなみに、元に戻すURLが見つからなかった場合でも (例えばテストではnilが返ってくることもあります)、リスト 13.52の||演算子でroot_urlをデフォルトに設定しているため、大丈夫です。
フィード画面のマイクロポストをテストする
まずはfixturesに別々のユーザーに紐づけられた投稿を用意しておく。
.
.
ants:
content: "Oh, is that what you want? Because that's how you get ants!"
created_at: <%= 2.years.ago %>
user: archer
zone:
content: "Danger zone!"
created_at: <%= 3.days.ago %>
user: archer
tone:
content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
created_at: <%= 10.minutes.ago %>
user: lana
van:
content: "Dude, this van's, like, rolling probable cause."
created_at: <%= 4.hours.ago %>
user: lana
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に関する統合テストを書いていく。
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
マイクロポストの画像投稿
source 'https://rubygems.org'
gem 'rails', '5.1.6'
gem 'bcrypt', '3.1.12'
gem 'faker', '1.7.3'
gem 'carrierwave', '1.2.2'
gem 'mini_magick', '4.7.0'
gem 'will_paginate', '3.1.5'
gem 'bootstrap-will_paginate', '1.0.0'
.
.
.
group :production do
gem 'pg', '0.20.0'
gem 'fog', '1.42'
end
まずは、carrierwave、mini_magick、fogをインストールする。
CarrierWaveを導入すると、Railsのジェネレーターで画像アップローダーが生成できるようになります。早速、次のコマンドを実行してみましょう
$ rails generate uploader Picture
次にmicropostにカラムを用意する必要がある。
具体的にはpictureカラムに画像ファイル名を保存するstring
$ rails generate migration add_picture_to_microposts picture:string
$ rails db:migrate
次にmicropostと画像を関連付け、pictureカラムを使って管理してねーという指示を出す。
以下
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 %>
最後にストロングパラメーターを使ってる都合上、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>
ここで注目すべきは、
<%= image_tag micropost.picture.url if micropost.picture? %>
picture?というメソッドはgemをインストールしたことで使えるようになる特殊なメソッド。
これを使うと、pictureカラムに参照先があれば、それをimgタグで表示してくれるというもの。
ここまでで、画像の表示もok
画像の加工や検証
このままだと、大きすぎる画像や、画像ファイル以外のものも送れてしまうのでそれを改善したい。
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
まずは、def extension_whitelistの部分のコメントアウトを外す。
そうするとバリデーションが発動し、jpgやjpeg gif png以外のものをアップロードしようとするとバリデーションに引っかかるようになる。
次に画像のサイズのバリデーションをかける
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
ここで注意したいのが、validateメソッドである。validatesではない。
これを使うと、指定したメソッドをバリデーションとして設定できる。
これで、5メガバイトより大きいサイズをアップロードしようとすると、バリデーションが発動する。
また、フロントエンド側もいじってみる。
<%= 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>
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>に注目していこう。
acceptとやると、指定した拡張子以外はそもそも選択できなくなる。
また、以下の部分にも注目
<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>
これで5メガバイト以上のファイルを選んだ時点で警告が出るようになる。
画像のリサイズ
画像をリサイズするためには、画像を操作するプログラムが必要になります。今回はImageMagickというプログラムを使うので、これを開発環境にインストールします (13.4.4でも説明しますが、本番環境がHerokuであれば、既に本番環境でImageMagickが使えるようになっています)。クラウドIDEでは、次のコマンドでこのプログラムをインストールできます
$ sudo yum install -y ImageMagick
どうやらこれで画像をリサイズするための機能が使えるようになったらしい。
その機能をアップローダーにincludeする
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
400,400のとこに注目。
例えば、xが400,yが800というファイルだったとする。
そのファイルをアップしようとすると、x200,y400に縮尺を保ったまま小さくすることができる
本番環境での画像アップロード
herokuの場合、アップロードしてもいいけどこちらの都合で勝手に消しますねーという仕様なので、
awsのs3?というものにアップロードする必要があるらしい。
その設定をしていこう
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
ここからはawsにクレカ登録して金払わなくてはいけないので、動画見る。