プログラミング年少組のど素人が、アウトプット用に素人目線の解説を書いていきます。
自分が自分に教えていることを想像しながら進めていきます。
下記と同じような境遇の方であれば参考になるかも
- まっさらなプログラミング初学者目線
- オール独学
- 勉強は得意じゃない
- 社会人のため、1日に勉強できる量は限られる。
電子書籍のwebテキスト(第6版)を購入して使用
Progateの「Web開発パス」完走済
ドットインストールのプレミアム会員卒業
Railsチュートリアル解説動画を見ながら学習中
#13章 ユーザーのマイクロポスト#
これまでの章ではほぼ全てのWEBアプリに搭載されているコアとなる機能を見てきました。
ユーザー(Usersリソース)
セッション(Sessionsリソース)
アカウント有効化(AccountActivationsリソース)
パスワードリセット(PasswordResetsリソース)
残りの章で「マイクロポスト(投稿機能)」や「フォロー」・「いいね」を付けて、Twitterのミニクローン的なサイトに仕上げます。
陶芸で言えば
これまでの章・・・土の準備
これからの章・・・土を食器の形に作り上げる
といった感じでしょうかね
#13.1 Micropostモデル#
いつでも最初に行うのはトピックブランチの作成です
$ git checkout -b user-microposts
##13.1.1 基本的なモデル##
まずは**「Micropostモデル」です
現在あるのが「Userモデル」ただ一つでしたが、ついに新しいデータベースを作成します。
もちろん保存されるのは投稿(content)**です。
まあ、その他のカラムではおなじみの「id」とか「created_at」、「updated_at」などもついてきます。
どのユーザーの投稿であるか紐づける「user_id」もあります。
モデル作成なので、2回目登場の「rails generate model ~」を使用します
$ rails generate model Micropost content:text user:references
「content」が投稿を指しますが、**「text型」**を使ってます。
String型でも255文字いけるみたいですし、データベース上でのパフォーマンスもさして変わらないみたいですが、文字数もたくさん取れるText型の方が将来的に柔軟的に使えるみたいなのでこちらを採用してます。
また、user:referencesとか言う見慣れないものがついていますが、これは新しくできた「models/micropost.rb」ファイルを開いてみると何となく見えてきます
class Micropost < ApplicationRecord
# 自動的に追加してくれる
belongs_to :user
end
こんなの書いた覚えもないのに、自動的に追加されてます。これが「user:references」の力です。
user:references・・・ユーザー関連か
belongs_to :user・・・ユーザー所属か
なんか似たような意味合いですよね。
これについては2章でやってましたね。「belongs_to」とか「has_many」とか
user:referencesを利用することで、自動的にインデックスと外部キー参照付きのuser_idカラムが追加され、UserとMicropostを関連付けする下準備をしてくれるという優れものです。
ですので
$ rails generate model Micropost content:text user_id:integer ...
なんてせずとも、関連するのに必要なことを自動でやってくれる「users:references」。素晴らしい。
ここで「rails generate model ~」でできていたマイグレーションファイルを見てみましょう
def change
create_table :microposts do |t|
t.text :content
# 「user:references」で追加されるもの
t.references :user, null: false, foreign_key: true
t.timestamps
end
# 懐かしのindexを追加
add_index :microposts, [:user_id, :created_at]
end
ちなみに「user:references」で追加されるものが以下です。
t.references :user, null: false, foreign_key: true
先ほど書いた「belongs_to」とかに加えて、「データにnullはいかん」、「外部キーと接続する」。などといったことも自動的にやってくれているのです。
また、indexを追加しています。こちらは自分で追加してます。
コンピュータがデータを検出する際に毎回頭から全部辿るようなことをせずに、検索ができるようにするってやつでしたよね。6章で出てきました(6章で導入した時は、メールアドレスの重複を避けるためでしたけどね)
add_index :microposts, [:user_id, :created_at]
2つになっているのは、「基本、索引する時は2つセットで出すから」だそうです。
最後にデータベース関連の設定を追加した時忘れてはいけないのがこれ
$ rails db:migrate
これでめでたくMicropostモデルが追加されているはずです
##13.1.2 Micropostのバリデーション##
これから、Userモデルでいろんなルール決めたように、Micropostモデルでもバリデーションで規制を決めていきます
機能の実装はもちろんテスト駆動開発で行います(先にテスト書くやつ)
# fixtureからデータを引っ張ってくる
def setup
@user = users(:michael)
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
end
いつものセットアップから始めます。
ユーザーをまいけるさんにして、投稿した内容を@micropost
に入れています。
紐づけるユーザーも忘れずに入ってますね。
続いてテストです
test "should be valid" do
# @micropostは有効ですよね?
assert @micropost.valid?
end
test "user id should be present" do
# @micropostのuser_idをnilにすると
@micropost.user_id = nil
# @micropostは有効ではないですよね?
assert_not @micropost.valid?
end
この内容ですと、特に何のバリデーションもかけなくても通るテストになります
ここからUserモデルでやったように、投稿にルールを設けていきます
test "content should be presrnt" do
# 空白の文字列を投稿
@micropost.content = " "
# その投稿は無効ですよね?
assert_not @micropost.valid?
end
test "content should be at most 140 characters" do
# 文字数が141文字の投稿
@micropost.content = "a" * 141
# その投稿は無効ですよね?
assert_not @micropost.valid?
end
おなじみのやつです。
空白と文字制限です。バリデーションを入れてないので今の段階でテストは失敗します。
Micropostモデルにバリデーションを書いて成功へと導きましょう
# 入れた方が分かりやすいので入れている
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140}
これもまたおさらいです。
が、「validates :user_id, presence: true」についてですが
確か「user:references」入れた時に「null: false」入ってたから、これはいらないかと思ってましたが。。。テキストに載ってるマイグレーションファイルには逆に「null: false」が抜けてましたけどね。
それが原因でバリデーション入れたのかな。
##13.1.3 User/Micropostの関連付け##
マイクロポストとユーザーの関係は至って単純です
マイクロポストは誰か特定のユーザーに紐付けられますし(belongs_to)
ユーザーは投稿した分だけマイクロポストを持つことになります(has_many)
この関連付けをすることで、各投稿についてこんな感じで書くことができます
Micropost.create
=> user.microposts.create
Micropost.create!
=> user.microposts.create!
Micropost.new
=> user.microposts.build
これで、紐づいているユーザーを通してマイクロポストを作成することができます。
「create!」って何でしょうかね。
→失敗時に例外を発生するらしいです
〜補足〜
newとcreateの違いは、遥か昔に戻りますが、newはオブジェクトを返すだけでデータベースに保存されませんが、createは保存までやってくれます。
@user.microposts.buildのようなコードを使うためには、さっき言った通り「has_many」「belongs_to」を明記する必要があります
# 投稿は一人のユーザーを持つので単数形
belongs_to :user
# ユーザーはたくさんの投稿を持つため複数形
has_many :microposts
よってこいつを修正します
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
@micropost = @user.micropost.build(content: "Lorem ipsum")
これでスッキリしました。
##13.1.4 マイクロポストを改良する##
ここでやること
①ユーザーの投稿を特定の順番で取得できるようにする
②ユーザーが削除されたら投稿も消えるようにする
それでは「①ユーザーの投稿を特定の順番で取得できるようにする」から見ていきましょう
ブログやTwitterのように投稿が新しい順に並ぶようにします
ここではdefault scopeとかいうテクニックを使うそうです
基本テストから書いていきますので、まずはテストです
test "order should be most recernt first" do
# fixtureの投稿microposts(:most_recent)とデータベースの投稿Micropost.firstが同じ
assert_equal microposts(:most_recent), Micropost.first
end
これは、データベース上の最初の投稿と、fixture内の投稿(:most_recent)とが同じであるか検証しています。
理解するのにだいぶ手間取りましたが、最新の投稿をMicropost.firstに**「これから」**する。という話です。
投稿のfixture/microposts.ymlはまだ作ってなかったので作りましょう
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael
:
:
とりあえず最初の1つを見てみます
投稿内容「content」、投稿日時「created_at」、投稿者「user」をそれぞれ指定しています。
基本投稿日時は指定できませんが、これはテスト用なので可能となってます。
話を最後の投稿がデータベース上で一番新しいidになる設定に戻しましょう
デフォルトでは投稿された順にidがつくようになっています。ユーザーidがまさにそうでした(そのせいで大分混乱してましたが・・・)
要は昇順がデフォルトになっています。
そいつを、特定の順序に並べるために登場するのがdefaut_scopeメソッドです。
「使い方がとにかく特殊」です
default_scopeの引数にorderを与えるのですが、
例えば保存した順番に並べる時は
order(:created_at)
となります。あれ?orderは引数として使われるんじゃ・・・
これを昇順に並べるならASCをつける
降順に並べるならDESCをつけます。
今回は降順に並べたいので
order('created_at DESC')
となります。ハッシュから文字列に変わってる・・・
この書き方は古いらしいのでこう書きましょう
order(created_at: :desc)
全然違いますね。ハッシュになったり、文字列になったり、小文字になったり、バラッバラです。
ここからが、「使い方が特殊」と書いた根拠になります
Micropostモデルに実装したのが、こちら
belongs_to :user
default_scope -> { order(created_at: :desc) }
これが、ラムダ式とかいう文法らしくて矢印のような「->」を使います。
ここで使われるProcという技術はRubyの概念で少し高度なものになるらしいので、あまり突っ込まないどきますが、コードのかたまりを変数に入れることができる。というものだそうです。
ここは、「default_scope」を使うことで、デフォルトの並び順を変更できるんだ〜へ〜くらいに止めときます。が、少しコンソール開いてProcで遊んでみましょう
aにTime.nowをそのまま入れると
>> a = Time.now
=> 2021-04-14 22:03:50 +0000
>> a
=> 2021-04-14 22:03:50 +0000
>> a
代入した時点の時間が出てきます
それをProcに入れると
>> a = -> { Time.now }
=> #<Proc:0x00007f16e0cd3ae0@(irb):5 (lambda)>
>> a
=> #<Proc:0x00007f16e0cd3ae0@(irb):5 (lambda)>
>> a.call
=> 2021-04-14 22:04:24 +0000
>> a.call
=> 2021-04-14 22:04:29 +0000
callで呼び出した時点の時間が出てきます
まさに、コードのかたまりを保存して、実行する時点で毎回呼び出しています。
説明してなかったですが、「call」で呼び出します。
default_scope -> { self.order(created_at: :desc) }
先ほどのこれに置き直して考えてみると、default_scopeを呼び出すたびに毎回並び順を降順にする。
という動作になるということでしょう。ええ〜と「call」は・・・
ま、とりあえず「order(created_at: :desc)」とすることで、当初から目的にしてた「後から保存した順に並んでいる状態」にすることができました。
##Dependent: destroy##
なぜかここで節が区切られてなかったので、自分で勝手に区切りました。
「②ユーザーが削除されたら投稿も消えるようにする」を見ていきましょう
ここで新しい概念が登場します(そんなに複雑ではないですが・・・)
サイトの管理者がユーザーを削除したら、そのユーザーが行った投稿も削除されるべきですよね
という当たり前の話です
実装も大したことありません
ユーザーは複数の投稿を持つ「has_many :microposts」にその機能を付け加えたらいいだけです
has_many :microposts, dependent: :destroy
これはテスト駆動開発で搭載しないんですね。
ですがテストは書きます
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
ここで、注目するのは「models/user_test.rb」でテストを書いてるということです。
テストの中でもfixtureの中でもまいけるさんの投稿は「content: "Lorem ipsum"」 ただ一つなので「'Micropost.count', -1」で通ります。
恥ずかしい話、私最初「models/micropost_test.rb」でテストを書いてしまっていたものだから、「'Micropost.count', -1」が通りませんでした。
なぜならMicropostのテストですと、まいけるさんはfixtureの中で4つ、テストの中で2つで合計6つの投稿を書いてることになっているので、そのまいけるさんをdestroyするという条件では「'Micropost.count', -1」では通らず、「'Micropost.count', -6」にすると通ってました。
「テキスト間違ってんジャーン」と勝手に思ってましたが、愚かなのは私だった。という話です。。。
#13.2 マイクロポストを表示する#
これからは投稿をユーザーのshowページに表示させ、テストします。
だんだん完成に近づいている感じしますね
##13.2.1 マイクロポストの描画##
先ほど投稿を表示させると言ったshowページはこれ「views/users/show.html.erb」です!
表示させるのは、「投稿」や「投稿した数」です。
実装手順については10章で、indexページでユーザー一覧を表示させる際に行った手順に似ています。
まずはMicropostコントローラーとビュー作成のためにコントローラーを作成します
$ rails generate controller Microposts
今回の実装はindexページと似ているということですが、どこが似ているかというとここです
<ul class="users">
<%= render @users %>
</ul>
@users
はUsersコントローラーのindexの部分に記載されてます・・・
ここでは
<%= render @users %>
とすることで、Railsが自動的に「_user.html.erb」というパーシャルを探しにいくということでした・・・
もちろん覚えていま・・
ということで今回も同じようにshowページに投稿を差し込みましょう
<ol class="microposts">
<%= render @microposts %>
</ol>
似てます。ほぼ同じです。
「ul」が「ol」になっているくらいでしょうか。
これは投稿については特定の順番で並ぶように設定したことが影響しています。
せっかくProcとかラムダ式とか知らない用語と戦いながら苦労して実装したので活かしましょう。
同じ形なので、もちろん「_micropost.html.erb」パーシャルが必要になるはず!
# 各投稿にid振り分け
<li id="micropost-<%= micropost.id %>">
# size50の画像で、クリックするとユーザー詳細に飛ぶリンクになってる
<%= 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>
表示されるのは「投稿」の他、「画像」「ユーザー名」「投稿のタイムスタンプ」が入ります。
そして「画像」と「ユーザー名」にリンクがついています。
が、そもそもこれが表示されているのはユーザー詳細ページです。
そして「画像」と「ユーザー名」のリンク先もユーザー詳細ページです。
これ、同じページが表示されるリンクなら、リンクにする意味なくね・・・?
ちなみにタイムスタンプは「〜分前に投稿」みたいな表示をするやつです。
次に進みます
投稿数が多くなってきた場合の処理についてです。
ユーザー数が多くなった場合の対処を今回も同じように使います
**「will_pagenateメソッド」**を使います
<%= will_paginate %>
前回使った時はこれを差し込んだだけでした
今回はこうなります
<%= will_paginate @microposts %>
micropostsのインスタンス変数を渡しています。
これはデフォルト(何も引数を渡さない)状態ですと、現在Usersリソースの中にいるため(users/show.html.erb)、ユーザーのデータが複数あればpaginationかける動作になってしまいます。
今回はあくまで、投稿データが複数あった場合にpaginationかけて欲しいので引数(@microposts
)を渡しています。
とは言ってもmicropostsのインスタンス変数は定義されていないのでいきなり使うことはできませんので、Usersコントローラーのshowアクションで定義します
def show
@user = User.find(params[:id])
@microposts = @user.microposts.paginate(page: params[:page])
end
ここでも「belongs_toとhas_manyのおかげ」で関連付けができるメソッドが使われています
@user.microposts.paginate(page: params[:page])
「@user.microposts」がつくことで、データベースから指定のuser_idの投稿を引っ張ってきます
paginate(page: params[:page])は30個ひとかたまりのページにします
※これは10章で全く同じものが出てきていました
当時は「User.paginate ( page: params[:page] ) 」となってました。
まとめますと、データベースから指定のuser_idの投稿を引っ張ってきて、30個ひとかたまりのページにして@microposts
というインスタンス変数に渡しています。
それが、最終的にここ👇に行くというわけです。
<%= render @microposts %>
さて次の課題は「ユーザーの投稿件数を表示させる」です
指定したuser_idの投稿数を返すには「count」を使います。これまで何度も出てきましたが、数数える系は大体「count」でした。
これを、おなじみの「user.microposts」に付ければいいだけです。
よってこうなります
user.microposts.count
ここまできてやっとユーザーのshowページに投稿を表示させる準備が整いました
:
:
<div class="col-md-8">
# 投稿があった場合以下を表示させるようにする
<% if @user.microposts.any? %>
# 投稿数はここで使う
<h3>Microposts (<%= @user.microposts.count %>)</h3>
# 「ol」で表示順まで指定して投稿を表示
<ol class="microposts">
# 表示する個別の投稿内容は「_micropost.html.erb」から引っ張ってくる
<%= render @microposts %>
</ol>
# paginateで投稿が30を超えるとページにして表示するようにする
<%= will_paginate @microposts %>
<% end %>
</div>
##13.2.2 マイクロポストのサンプル##
投稿をどのように表示するかを決めましたので、今度はそれが実際にどのように表示されるかを見ていきましょう
サンプルデータといえば「seeds」を編集します
今回は最初の「6人」のユーザーが「50」個の投稿を行ったというデータを追加します
※全員にしちゃうと時間がかかりすぎるし、あくまで表示内容見るだけなので全員分は必要はない。かと言って一人だと心配なので、とりあえずの「6人」です
最初の「6人」のユーザー抽出についてはこうします
User.order(:created_at).take(6)
分解して見ていくと
order(:created_at)
これはidの若い順番で引っ張ってきます
take(6)
6人使うよ!ということを表すメソッドです
サンプルで使うダミーのデータは以前も登場した「Faker gem」を使用します
ダミー投稿については「Faker gem」にある「Lorem.sentence」というメソッドを使います
それらを使った出来上がりがこんな感じです
# 最初の「6人」のユーザー抽出して「使います」
users = User.order(:created_at).take(6)
# 以下を50個回繰り返します
50.times do
# 投稿内容は5つの単語を組み合わせてダミーを作成
content = Faker::Lorem.sentence(word_count: 5)
# ユーザーそれぞれにダミー投稿を追加していく
users.each { |user| user.microposts.create!(content: content) }
end
「each」の使い方が今までに見たことない感じですが・・・
ま、とりあえずこれを反映させましょう
# いったんリセットして
$ rails db:migrate:reset
# いざ反映
$ rails db:seed
この状態で開くと見た目がグチャグチャなので、CSSも整えときます(テキストコピペ・・・)
##13.2.3 プロフィール画面のマイクロポストをテストする##
プロフィール画面に表示される投稿内容について統合テストを書いていきます。
まずはテストファイル作成から
$ rails generate integration_test users_profile
先ほどの流れをテストで書いていくなら、テスト用のデータが必要になります(seedsはテストでは使えません)
テストといえばおなじみのfixtureに作ります。
とは言っても内容はseedsのやつとほとんど同じです
:
:
<% 30.times do |n| %>
micropost_<%= n %>:
content: <%= Faker::Lorem.sentence(word_count: 5) %>
created_at: <%=42.days.ago %>
user: michael
<% end %>
ほとんど同じですね。
eachを使わずにブロック変数 | n | を使って、do ~ end を30回繰り返す形にしている感じですかね。
上に4つ記事を書いた形跡があるので、テストで使われる投稿は全部で34てとこですね。
準備が整いましたので、テストの内容を見ていきます
class UsersProfileTest < ActionDispatch::IntegrationTest
# これがないとapplicationヘルパーで定義した「full_title」がテスト内で使えません!
include ApplicationHelper
def setup
# @userをまいけるさんとしてテストします
@user = users(:michael)
end
test "profile display" do
# プロフィール画面(show)にアクセスします
get user_path(@user)
# showページが表示されてますよね?
assert_template 'users/show'
# HTMLの"title"タグには「full_title(@user.name)」が含まれてますよね?
assert_select 'title', full_title(@user.name)
# HTMLの"h1"タグには「text: @user.name」が含まれてますよね?
assert_select 'h1', text: @user.name
# HTMLの"h1"タグ内にgravatarクラス付きのimgタグが含まれてますよね?
assert_select 'h1>img.gravatar'
# 投稿の投稿数を表示していますよね?
assert_match @user.microposts.count.to_s, response.body
# HTMLの"div"タグに「pagination」ありますか?
assert_select 'div.pagination'
@user.microposts.paginate(page: 1).each do |micropost|
# ページ内の全てのHTML内に投稿を表示してますよね?
assert_match micropost.content, response.body
end
end
スッと理解できなかったのがここ
assert_match @user.microposts.count.to_s, response.body
んん〜〜バラしましょう
assert_match
第一引数の正規表現の値と第二引数の値が同じであればOKになるテストです
正規表現て・・・6章で出てきたこんなやつ「/\A[\w+-.]+@[a-z\d-.]+.[a-z]+\z/i」・・・よくわからんね
この場合だと「@user.microposts.count.to_s」が第一引数「response.body」が第二引数にあたります
@user.microposts.count.to_s
またまた登場しました。belongs_toとhas_manyのおかげで使えるメソッドシリーズ。
@user
の投稿数を数字に変換してます。数を数字に変換て、変な感じしますが・・・
response.body
これは「そのページのHTML本文をすべて返すメソッド」です
これで納得です(無理やり)
@userさん
の投稿の投稿数を表すものが、HTML全体を見渡して存在していればOKになるというテストです
補足です
テストの冒頭にあるこちら
include ApplicationHelper
これを忘れていたために、「full_title」の部分ができねーできねーでずっと引っかかってました。
「applicationヘルパー」で定義したヘルパーメソッドなんかは、
「include ApplicationHelper」
と、わざわざ使うことを宣言しないとテスト内では使えません。。ご注意を。
#13.3 マイクロポストを操作する#
さあ、ここからやっと投稿する!という機能を盛り込んでいきます
投稿を書く画面については、HOME画面上に表示されるように設計します。
よって、Micropostsコントローラーにはnewやeditのようにビューを呼び出すアクションが不要です。
「microposts/new.html.erb」や「microposts/edit.html.erb」とかはないってことですね。
投稿のためのや、投稿を削除するためのビューが存在しているのではなくて、「もうすでに作成してあるビュー(HOME画面上)の中に投稿する場所を入れ込みます」
ですので、Micropostsコントローラーに作るアクションは「create」と「destroy」の2つでOK〜ということです。
てなわけで、早速ルーティングから始めちゃってます
resources :microposts, only: [:create, :destroy]
宣言通り、「create」と「destroy」の2つしか作成していません。
##13.3.1 マイクロポストのアクセス制御##
当たり前ですが、投稿や投稿の削除を行うユーザーは最低限ログイン状態でなくてはなりません
要は「createアクション」や「destroyアクション」を利用する時点でログイン済みである条件が必要です・・・ということはbefore_actionです
恒例ですが、テストから書いていくTDDで進めていきます。
今回のテストはコントローラーなので「機能テスト」ですね
def setup
# fixtureで最初にある投稿を@micropostインスタンスとして使う
@micropost = microposts(:orange)
end
test "should redirect create when not logged in" do
# do以下を実行しても「投稿数」は変わらないですよね?
assert_no_difference 'Micropost.count' do
# 「microposts_path(createアクション)」に投稿内容(content)をPOSTリクエスト(送信)する
post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
end
# ログインページに飛ばされますよね?
assert_redirected_to login_url
end
test "should redirect destroy when not logged in" do
# do以下を実行しても「投稿数」は変わらないですよね?
assert_no_difference 'Micropost.count' do
# 投稿に対して「DELETE」リクエストを送る
delete microposts_path(@micropost)
end
# ログインページに飛ばされますよね?
assert_redirected_to login_url
end
上記の試験は「ログインせずに」投稿したり、投稿を削除したりしようとすると・・・できないですよね!という内容になってます。
もちろんこの時点ではテストは失敗します。。。
「アクションを使用する前にログインしてくださいね」。。という制限はUsersリソースので登場しました
before_action :logged_in_user, only: [:index,:edit, :update, :destroy]
このlogged_in_userメソッドがまさにそうです
# ユーザーのログインを確認する
def logged_in_user
# ログイン状態でない限りは
unless logged_in?
# フレンドリーフォワーディングのために一時的にセッションにURLを保存する
store_location
# 「ログインしてね」のフラッシュを表示するよ
flash[:danger] = "Please log in"
# ログインページに飛ばすよ
redirect_to login_url
end
end
これを今回のMicropostsコントローラーでも使いたいのですが・・・
現在はUsersコントローラーの中で定義しているので、Micropostsコントローラーでは使えません。
ですので他のコントローラーでも使えるようにみんなの親玉であるApplicationコントローラーに移動させちゃいましょう。
そのままペッとコピペするだけでOKです。
被っちゃうのでコピー元のUsersコントローラー内の方は消しちゃいましょう
仕上げはMicropostsコントローラーにbefore_action入れるだけで実装完了です
before_action :logged_in_user, only: [ :create, :destroy]
簡単ですね〜〜
これでテストを通過できるようになりました。
##13.3.2 マイクロポストを作成する##
話はどこで投稿を作成するか問題に戻ります。
それについては、最初に「micropost/new」的な投稿するためのページや、投稿を削除するためのページが存在しているのではなくて、「もうすでに作成してあるビューの中に投稿する箇所を入れ込む」と書いていました。
肝心の「それはいったいどこの部分よ!」ということは明記していませんでしたが、これからそれに取り掛かります。
それは・・なんと・・・HOME画面です!(root_pathで開くページですね)
今のところ、よくよく考えたら変な感じに仕上がっています。
ログインしてHOME画面開いたら、真ん中にデカデカと「Sign up now!」なんて書いてるのっておかしいですよね。
ログインしてるのに、「登録しよう!」なんて言われても、「もうしとるわ!」とツッコみたくなります。
今回は、ログイン後のページには「Sign up now!」を表示させずに、投稿フォームを表示させるように機能を変更します。
まずはMicropostコントローラーのcreateアクションを動かしましょう
def create
# 新しく作った投稿をインスタンス変数に入れます
@micropost = current_user.micropost.build(micropot_params)
# 投稿を保存したら
if @micropost.save
# フラッシュを表示してね
flash[:success] = "Micropost created!"
# HOME画面に戻ってね
redirect_to root_url
else
# 失敗したら再度投稿ページに戻って指定のviewファイルを表示してね
render 'static_pages/home'
end
end
private
# ストロングパラメーター!
def micropost_params
params.require(:micropost).permit(:content)
end
belongs_toとhas_manyのおかげで使えるメソッドシリーズがさらに改良されて登場しています
current_user.micropost.build(micropst_params)
登場時ははこんな形だったかと思います => user.microposts.build(arg)
userがcurrent_userになっただけですね。
userはuserでもログインしたuserということでcurrent_userを使っています
受け取る引数もmicropot_paramsということで、おなじみのストロングパラメーターから許可された投稿のみを抽出しております。
:HOME画面に投稿フォームを追加・・じゃなくて設定します。
もともとのHOME画面のビュー画面を、ログインしている場合としていない場合に切り分けて作成していきます
# ログインしている場合は?以下に続く
<% if logged_in? %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
# 指定のviewファイルを表示させる = ユーザーのプロフィール
<%= render 'shared/user_info' %>・・・パーシャル①
</section>
<section class="micropost_form">
# 指定のviewファイルを表示させる = 投稿フォーム
<%= render 'shared/micropost_form' %>・・・パーシャル②
</section>
</aside>
</div>
<% else %>
ログイン画面を表示
<% end %>
条件で指定していましたログイン状態の場合に投稿フォームを表示するために
<% if logged_in? %>(ログイン状態だったら)を使っています。
表示するものを2つのパーシャルに分けています
①ユーザーのプロフィール(プロフィールへのリンクと投稿数)👉サイドバーに配置
②投稿フォーム👉画面中央に配置
これは、一つの画面に2つのパーシャルを組み込む今までになかったパターンです
①についてはこちら
# ログインしているユーザーの画像を表示
<%= 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」が入ってますね。
英語の複数形表示に柔軟に対応してくれるやつでした。
ちなみにこいつ「current_user.microposts.count」もbelongs_toとhas_manyのおかげで使えるメソッドシリーズです!あ〜既出でしたか
続いて②の投稿フォームです
# いつものフォームの「form_with」
<%= form_with(model: @micropost, local: true) 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 %>
うう〜ん
エラーのやつに何かへんなのくっついてますよね・・・
その前にform_withについてる@micropost
を使えるようにします
<%= form_with(model: @micropost, local: true) do |f| %>
インスタンス変数は、そのページを呼び出すアクションで定義しますので、今回そのページを呼び出しているのは・・・static_pagesコントローラーのhomeアクションです!
ここら辺の初めの方にしか取り扱わなかったコントローラーやアクションを掘り起こすのは全体像を理解し直すのにいい機会です
def home
@micropost = current_user.microposts.build if logged_in?
end
条件後付けの後置if文ですね!
あれ、でもcurrent_user自体がログインしてる状態を指していたような・・・その上で「logged_in?」メソッド入れているということかな?
# ユーザーがログインしていればtrue、その他ならfalseを返す
def logged_in?
!current_user.nil?
end
それでいうと「current_user.microposts.build if logged_in?」は
「ログインユーザーが投稿を作成 もしそのユーザーがログインしていれば」って意味わからん
いやいや違います。
「if logged_in?」をつけているからこそ、「current_user」が使えてるのです
Micropostコントローラーのcreateアクションで使った時もbefore_actionでログイン状態にしてましたし
HOME画面でもパーシャル2つ作った時も、条件分岐の最初は<% if logged_in? %>から始まってました。
それでは先ほど気になったこれを見ていきましょう
<%= render 'shared/error_messages', object: f.object %>
これまでにこれを使っていた際はこうなっていたはず
<%= render 'shared/error_messages' %>
この謎は引用元のerrorパーシャルを見てみれば分かります
<% if @user.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(@user.errors.count, "error") %>.
</div>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
このように、パーシャル自体が@user
変数を直接参照していました。
これだと「user」のエラーは出せるけど、「micropostやその他」のエラーは知らん。という状況です。
今回は@user
だけではなくて@micropost
でもエラー表示機能を使いたいという話です。
ここでフォーム変数なるものを使います。それがf.objectてやつです。
f.objectについては
form_with(model: @user, local: true) do |f|
の場合f.objectは@user
になります
form_with(model: @micropost, local: true) do |f|
の場合f.objectは@micropost
になります
変幻自在ですね。変数だから!
今回の課題は、エラーメッセージパーシャルに@micropost
オブジェクトを渡したい!です
これまでのエラーメッセージパーシャルでは@user
が使われてたので、@user
オブジェクトしか渡せませんでした。
そこで今回利用されていたのがこれ「object: f.object」
時と場合で変化するobjectをエラーメッセージパーシャルに入れ込みましょう
<% 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 %>
なんということでしょう・・・form_withフォーム内のインスタンス変数の値が@user
の場合にしか対応していなかったものが、その場の状況に応じてエラーメッセージを作れるような対応に変わりました。
もう少し詳しく言いますと、「object」がformに入れるインスタンス変数を「f.object」から情報を抜き取ってエラーメッセージパーシャルに対応させている形です。逆に分かりにくいっすね。。。
もちろんエラーメッセージパーシャルは@user
専用機として使っていましたので、
<%= render 'shared/error_messages' %>
が通用していました。
が、今のエラーメッセージパーシャルはobjectに何を入れるかを指定しないと何のエラー分からず、参照にするものがわからないため、「object: f.object」をパーシャルの呼び出しの際にくっつける必要が出てきました。
これまで上記のようにしていたのを全部このように変更しましょう
<%= render 'shared/error_messages', object: f.object %>
これで完成です!
エラー表示にも対応した投稿画面ができました!
もちろんテストも成功します
##13.3.3 フィードの原型##
前の節で「投稿を作成する」ところまでできました。
これからの節で**「投稿をいい感じに表示する」**という修正をします。
今のところ、一応作成した投稿はユーザーが自身のプロフィールページまで飛ぶと見れるようになってますが、正直面倒です。
出来立てホヤホヤの投稿はすぐに見たい!っていうか投稿履歴自体が投稿ページにあったら問題は一気に解決です。
という搭載をやっていきます。
いきなりフィードという単語が出てきます。
馴染みのない単語でしたが、テキストでは知ってて当然ということを前提に、当たり前に使ってますので調べてみました👉feed
抜粋しますと
外部から刻々と配信されてくるデータの流れや、これを時系列に一覧できるよう整理した操作画面などのことを指すことが多い。
単なる一覧表示のことじゃないですか。
分かれば簡単です。
feedメソッドを作るとあります。なるほど、これが節の始めに言ってた投稿履歴を一覧で並べてくれる役割を持ったメソッドですね。
てことで、こいつをUserモデルで定義します(フィードを持っているのはあくまでユーザーですから)
def feed
Micropost.where("user_id = ?", id)
end
whereメソッド
これまでデータベースからデータを引っ張ってくる場合は、findとかfind_byを使ってましたが、所詮こいつらは1つのデータしか引っ張ってこれません。
feedの機能としては該当するデータを根こそぎ引っ張ってくるもっと強力な、検出メソッドが必要です。そこでwhereメソッドです。こいつは与えられた条件にマッチするレコードをすべて返す優れものです。
引数が("user_id = ?", id)とすることで、SQLインジェクションと呼ばれるリスクに対する対策になるらしいです。
とりあえずは、条件が第一引数で提示した「"user_id = ?"」に該当するものは全部引っ張り出しています。
そして、第二引数の「id」がそれぞれのユーザーのidに該当します。
それが、第一引数の「?」に割り当てられます。
「id」が2の人のデータを引っ張り出すには
Micropost.where("user_id = 2")ということになり、投稿記事の中からuser_idが2のやつを全部引っ張ってきます。
SQLインジェクション
サラッと出てきましたが、セキュリティ上タダでは済まされない問題ですのでこれが何かを紹介しておきます。
アプリケーションのセキュリティ上の不備を意図的に利用し、アプリケーションが想定しないSQL文を実行させることにより、データベースシステムを不正に操作する攻撃方法のこと。 また、その攻撃を可能とする脆弱性のことである。
wikipedia引用
データベースは機密性の高いデータの宝庫ですから不正に操作なんかされたら完全にジ・エンドです。
ここの対策には万全を期することにしましょう。
準備ができましたのでこれから以下の手順でフィードの搭載に進みましょう
①homeアクションにフィード用のインスタンス変数@feed_items
を追加する(②のパーシャルで使うやつ)
②_feed.html.erbパーシャルを作成する
③Homeページに②で作成したフィード用のパーシャルを追加する
投稿フォームを追加した時と順番が逆になってますね。。。
①からです。この@feed_items
はパーシャルの中で使用することになるため、予め準備しておこうというワケです。インスタンス変数なので「実体」ってやつですね。
@micropost
と同様にhomeアクションに加えます。
def home
if logged_in?
@micropost = current_user.microposts.build
@feed_items = current_user.feed.paginate(page: params[:page])
end
end
**paginate(page: params[:page])**は以前も出てきましたね。
この最大30個ひとかたまりのページにするやつで、current_user.feedで特定ユーザーの投稿を全部引っ張ってきて、@feed_items
に渡す。という流れです。
そして後置if文がいつの間にか普通のif文に戻ってます。
1行の時は後置if文で、2行以上の時は前置if文にするという慣習があるみたいです。
②でパーシャル(部品)から作ります。
<% if @feed_items.any? %>
<ol class="microposts">
# 先ほど定義した@feed_itemsです
<%= render @feed_items %>
</ol>
<%= will_paginate @feed_items %>
<% end %>
出たー。まさかの部品の中に部品入れてるパターン。
これ、投稿フォームの時とはパーシャルの中身が全然違いますよね。。。
<%= render @feed_items %>
:まとめて(最大30個)ドバッと表示させて
<%= will_paginate @feed_items %>
:30個以上あれば下にページのやつを付ける
③で部品を本体(HOME画面)にはめ込みます
<div class="col-md-8">
<h3>Micropost Feed</h3>
<%= render 'shared/feed' %>
</div>
これで搭載完了です!と、言いたいところですが、サーバー開いて見ても一見問題なさそうですが落とし穴がいくつかあります。新しい機能を搭載したことによってズレが生じちゃってる感じです。
①空白など無効な投稿をした際にエラーになる
②paginateが正常に動作しなくなる
①から見ていきます
今のところ、投稿に失敗した場合の動作は
:
:
else
render 'static_pages/home'
end
これなので、static_pages/home
を表示しようとします。
そこで、上からstatic_pages/home
を辿るとこいつが出現します
<h3>Micropost Feed</h3>
<%= render 'shared/feed' %>
さらにパーシャルで飛ばされこいつを表示しようとします
<% if @feed_items.any? %>
:
:
長くなりましたが、ここで問題が発生します。
render 'static_pages/home'
というのは「static_page_controller」の「home」アクションにGETリクエストを送ることなくstatic_pages/home
を表示させる動作です
が、ここで@feed_items
が見つかりません。
ですので、microposts_controllerで投稿が失敗した時に表示する@feed_items
を定義しなくてはいけません。
else
@feed_items = current_user.feed.paginate(page: params[:page])
render 'static_pages/home'
end
内容は同じでOKですね。
ふ〜これで一個。
もう一つは**「投稿に失敗した後に表示されたHOME画面で」**②paginateが正常に動作しない問題です
createアクションの「else」より下で返されたこいつ
render 'static_pages/home'
辿り辿って最終的にはこのパーシャルに辿り着きます
:
<%= will_paginate @feed_items %>
<% end %>
これは投稿が存在していないindexアクションを開こうとしている状態になっているそうです。
そもそもstatic_pagesコントローラーにはindexアクションとかどこにもないけども。。。
これを解決するためには、どこのコントローラーのどのアクションをwill_patinateに渡すかを提示することで解決するそうです。
:
<%= will_paginate @feed_items,
params: { controller: :static_pages, action: :home } %>
<% end %>
と、いうことは、失敗後に表示されたwill_paginateはどこの情報を参照すればいいか分からない状況に陥ってたってことか。。ではなくて!
will_paginateのデフォルトの動作がUsersやMicropostsを参照にするという挙動があったから。らしいです。。ゆーても3rdパーティのgem特有の動作なのでそこまで詳しく覚える必要もないみたいです。
これでとりあえずフィードは完成しました。やっと!やっと次に進めます
##13.3.4 マイクロポストを削除する##
投稿作成が終わりましたので、投稿削除を追加します
一つ一つの投稿の表示については「_micropost.html.erb」で書いていましたので、ここに削除するための「delete」リンクを追加します(リンクはもちろんdestroyアクションにDELETEリクエストを飛ばします)
:
:
<% if current_user?(micropost.user) %>
<%= link_to "delete", micropost, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
DELETEリクエストを送る「delete」リンクができました。
**data: { confirm: "You sure?" }**はなんとなく分かります。
削除する前に「本当に消していいですか?」の確認トーストみないなのが出るのでしょう。
リンクができましたので、あとはdestroyアクションで@micropost
をdestroyすればよさそうです
def destroy
@micropost.destroy
flash[:success] = "Micropost deleted"
redirect_to request.referrer || root_url
end
request.referrer || root_urlは単なる新登場のメソッドです。
フレンドリーフォワーディング的な役割をするもので、1つ前のURLに戻す機能があります。
ですので、「request.referrer || root_url」とは「1つ前のURLに戻すかもしくは
HOME画面に戻す」ということです。
これでもちろん終わりではありません。。
以前、ユーザーの削除を搭載した時は管理者ユーザーのみに削除の権限を与える設定をしました。
ユーザー同士で退会させることができるサイトなんかカオスです。
投稿も同じです。誰かに自分の投稿が勝手に削除されたらたまったもんじゃありません。
今回は条件を変えて投稿した本人であれば削除できる!という条件にします。当然です。
まずは前回の管理者ユーザーフィルターを見ると
before_action :correct_user, only: [:edit, :update]
before_action :admin_user ,only: [:destroy]
:
private
# 正しいユーザーかどうかを確認
def correct_user # => こちらはdestroyアクション関係ない
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
# 管理者ユーザーでなければホーム画面に飛ばす!
def admin_user # => destroyアクションで使うのはこちら
redirect_to(root_url) unless current_user.admin
end
同じような感じでやればいいんですよ
before_action :correct_user, only: :destroy
:
private
# 削除しようとしている投稿が自分のものであるかを確認する
def correct_user # => 投稿を書いた本人がアクセスできるという条件を付けたい
@micropost = current_user.microposts.find_by(id: params[:id])
redirect_to root_url if @micropost.nil?
end
若干違いますよね・・・
前回のdestroyアクションはadmin_userでフィルターをかけていますが、こちらは「correct_user」でフィルターをかけています。
むしろ前回のdestroyアクションと同じというか、editアクションやupdateアクションと同じ位置付けです。
条件も同じですもんね。「ログインしている本人がアクセスできる」という条件としたい点が同じですから。
それを今回は改良して、**「投稿を書いた本人が削除対象の投稿を保有している = 削除したい投稿が自分のものであるか」**という条件としているワケです。
「current_user.microposts」:自分自身の投稿
desotroyしようとしてる投稿に対して、「あなたが削除対象の投稿を保有していなければ「delete」押しても、destroyアクションを動かさずに、HOME画面に飛ばします」となるワケです
##13.3.5 フィード画面のマイクロポストをテストする##
削除の機能についてはテストしてなかったですね。
テストが先になったり、機能の搭載が先になったり・・・
まずは機能テストからやっていきます
確認する機能は先ほど搭載したdestroyアクション関連です
その前に、fixtureで準備している投稿に、まいけるさん以外のものも加えます
:
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
あーちゃーさんとらなさんの投稿が加わりました。
それでは準備も整いましたので、テストを書いていきましょう。
自分の投稿ではないものは消せないことを確認しています
:
:
test "should redirect destroy for wrong micropost" do
# まいけるさんとしてログインします
log_in_as(users(:michael))
# 「micropost」を「:ants」とします(あーちゃーさんの投稿)
micropost = microposts(:ants)
# 投稿数は変わらないよね?
assert_no_difference 'Micropost.count' do
# 「micropost」をmicropost_pathにDELETEリクエスト送る
delete micropost_path(micropost)
end
# HOME画面に飛ばされるよね?
assert_redirected_to root_url
end
機能テストはこのくらいにして、統合テストでこれまでの内容を一気に見ていきます
そういえば、投稿の統合テストはまだ作成してなかったです
$ rails generate integration_test microposts_interface
これで中身を書いていきます
def setup
# @userをまいけるさんとします
@user = users(:michael)
end
test "micropost interface" do
# まいけるさんとしてログインします
log_in_as(@user)
# HOME画面にGETリクエスト飛ばします
get root_path
# ページネーションのHTML含まれますよね
assert_select 'div.pagination'
# 無効な送信
# do以下を実行しても投稿数は変わりませんよね
assert_no_difference 'Micropost.count' do
# POSTリクストをmicroposts_path(createアクション)に空白の投稿を入れて送信!
post microposts_path, params: { micropost: { content: "" } }
end
# エラーのHTMLが含まれますよね
assert_select 'div#error_explanation'
# 投稿に失敗した後のページネーションがおかしくなってないですよね
assert_select 'a[href=?]', '/?page=2' # 正しいページネーションリンク
# 有効な送信
# 「変数content」に投稿内容を代入
content = "This micropost really ties the room together"
# do以下を実行することで投稿数が変わりますよね
assert_difference 'Micropost.count', 1 do
# POSTリクエストをmicroposts_path(createアクション)に変数contentの内容で送信!
post microposts_path, params: { micropost: { content: content } }
end
# HOME画面に飛ばされますよね!
assert_redirected_to root_url
# テストも続いて飛びます
follow_redirect!
# 投稿を表すものが、HTML全体を見渡して存在していますよね
assert_match content, response.body
# 投稿を削除する
# HTMLにaタグの「delete」リンクが含まれますよね
assert_select 'a', text: 'delete'
# 「変数first_micropost」に1ページ目の最初の投稿を代入
first_micropost = @user.microposts.paginate(page: 1).first
# do以下を実行することで投稿数が変わりますよね
assert_difference 'Micropost.count', -1 do
# DELETEリクエストを「first_micropost」に対して送ります
delete micropost_path(first_micropost)
end
# 違うユーザーのプロフィールにアクセス(削除リンクがないことを確認)
# GETリクエストをあーちゃーさんのプロフィールページに飛ばします
get user_path(users(:archer))
# HTMLにaタグの「delete」リンクが「0個」含まれますよね
assert_select 'a', text: 'delete', count: 0
end
新しい概念も、初登場も、複雑な部分も何もないのでとっとと先に進みましょう
#13.4 マイクロポストの画像投稿#
テキスト投稿は実装できたので、画像の投稿にもチャレンジしましょう。と、いうことです。
が、これはあくまで投稿の応用編になるそうです。
本番環境にデプロイする際にはAWSの機能を利用するということで、railsではない部分にも関与している部分あり、そちらが原因でうまくいかないケースも大いにあり得ます。
今後自作するサイトでAWSも関わってきそうならじっくりやってもいいし、うまくいかなくてもあまり固執せずに次の章に進んでrailsチュートリアルを一旦完走して、また戻ってきてもいいみたいです。
とは言っても、本番環境にデプロイするまではAWSは登場しないですし、機能の搭載についてはしっかりやっていく必要がありそうです。
##13.4.1 基本的な画像アップロード##
画像をアップロードする仕組みについてはRailsに仕込まれているActive Strageという仕組みを使います。具体的にはrailsのバージョン5.2から導入されたらしい。これは、画像はもちろんPDFや音楽ファイル、テキストファイルなど幅広い形式のアップロードに対応しているみたいです。
これまでは3rd_partyのgemを入れて画像投稿の機能を入れていたが、Railsにその機能が標準装備されたのでその必要がなくなったというわけです。
$ rails active_storage:install
仕込まれているのにインストール?アプリケーションにはなかったので、追加したということです。。
ま、とりあえずこれで画像投稿機能追加のスタートラインに立ちました
これまで通り、「モデル」「ビュー」「コントーラー」の観点から搭載までの流れを見ていきます
まずは「モデル」
先ほどの「install」を実行すると何故だか分かりませんが、マイグレーションファイルができます。
これは、画像はもちろんPDFや音楽ファイル、テキストファイルなど幅広い形式のファイル保存に対応したマイグレーションファイルを作っています。
さっそく「モデル」が登場しました
現時点で中身を見て分からなくても特に問題はないみたいです。
と、言いつつものぞいてみると、いくつかのテーブルができてるみたいです。
様々な形式のファイルに対応するために必要なのでしょう!
とりあえず、マイグレーションファイルができたのならこちらを忘れずに
$ rails db:migrate
次は「ビュー」です
has_one_attachedメソッドを使って、Micropostモデルとアップロードされたファイルである「image」を関連付けます
belongs_to :user
has_one_attached :image
:
初登場のメソッドですが、書いた通り「指定のモデルと、アップロードされたファイルを関連付ける」機能を持っています。
has_one_attached:投稿1件につき画像は1件(今回はこちらを採用)
has_many_attached:投稿1件につき画像は複数
1つの投稿に対して何個の画像を付けることができるかを決めましたので、投稿フォーム「micropost_form.html.erb」に画像投稿する場所を加えます
:
<span class="image">
<%= f.file_field :image %>
</span>
画像投稿のフォームには専用のfile_fieldを使用します。これは覚えるしかありません。
テキストなら「text_field」でしたし、emailなら「email_field」。passwordなら「password_field」なんてものがありました。
最後に「コントローラー」です
createアクションを編集し、micropostオブジェクトに画像を追加できるようにします
ここでまた新しいメソッドattachメソッドが登場します
今回追加するものがこちらです
def create
@micropost = current_user.microposts.build(micropost_params)
@micropost.image.attach(params[:micropost][:image])
:
end
private
def micropost_params
params.require(:micropost).permit(:content, :image)
end
attachメソッドの説明は何もありませんが、なんとなく投稿のデータに「画像」を関連づける的な働きをしているのが分かります。いわゆる添付ってやつですね。
ですので、実際にmicropostsテーブルをのぞいてみても、「image」カラムができるわけではありません。
「image」カラムはあくまで、ActiveStorageをインストールした時に新しくできたテーブルに保存されており、attachメソッドによって各contentに関連付けされていた、というわけです。
ストロングパラメーターに追加
当然@micropost
が受け取るデータに「image」が増えましたので、ストロングパラメーターで許可してあげないと関連付けたところで弾かれちゃいます。
``controller/micropost_controller.rb
def micropost_params
params.require(:micropost).permit(:content, :image)
end
投稿に画像を追加することができましたので、あとは各投稿に表示できるようにしましょう。
```microposts/_micropost.html.erb
<%= micropost.content %>
<%= image_tag micropost.image if micropost.image.attached? %>
「image_tag」は画像ファイルへのパスですね。
「if micropost.image.attached?」をつけることによって、「関連付けされた画像がある場合に」という条件が付与されています。
一応ここまでで実装は完了です
「モデル」「ビュー」「コントローラー」にそれぞれ新しい機能を使うための修正を加えていくという点は今までやったことと同じです。
今後も機能の追加を行う場合はこれが基本ということを覚えておきましょう。
##13.4.2 画像の検証##
画像投稿の機能を追加したのですが、投稿する画像については特に何の規制もついていません。
バリデーションなしの無法地帯です。
超巨大なファイルサイズの画像も投稿可になっております。
が、肝心のActiveStrageにはバリデーション機能が備わってませんのでgemで追加します
gem 'active_storage_validations', '0.8.2'
これを入れたらお決まりの
$ bundle install
このgemを導入することによって、以下のような形でバリデーションを組むことができるようになります
validates :image, content_type: { in: %w[image/jpeg image/gif image/png],
message: "must be a valid image format" },
size: { less_than: 5.megabytes,
message: "should be less than 5MB" }
「content_type」と「size」において、それぞれルールを設けているみたいですね。
「content_type」のバリデーション
「jpeg,gif,png」の拡張子ファイルにすること。でないと"must be a valid image format"
なんて忠告を出すわよ
「size」のバリデーション
5メガバイト以下のサイズにしなさい。でないと、"should be less than 5MB"
なんて忠告を出すわよ
※今回のバリデーションの書き方はあくまで'active_storage_validations'という「gem」に対応したものとなっているので、他のgem入れた場合は別の書き方になる
モデル側の規制はこんな感じでOKです。
今度は**「ブラウザ側」**にも規制を加えて、バリデーションを強化します。
まずは画像サイズの制限についてです。
こちらはフロント側の領域になりますので、JavaScriptを加えることで対応します。
<script type="text/javascript">
$("#micropost_image").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.");
$("#micropost_image").val("");
}
});
</script>
これまでの知識を総動員してもなんも出てこねえです・・・なんか、5メガバイトを超えるとアラート出すのか・・・な・・くらいしか読み取れません・・・
テキストもJavaScriptは本書のメイントピックでもないし、あまり詳しくは突っ込みません的なことを書いてます。今回はその程度で読み取れればとりあえずはスルーしましょう。
最後に!
画像フォームにおいても拡張子を指定するルールを加えておきます
<span class="image">
<%= f.file_field :image, accept: "image/jpeg,image/gif,image/png" %>
</span>
何やら先ほど出てきた「jpeg,gif,png」の拡張子ファイルが出てきてますが、それらが**accept(承認)**ということで付け加えられています。
これをすることで、大きな違いが出てきます。。
それは、実際に投稿をしようとしてみればわかるのですが・・・
何と、画像投稿に関する制限に引っかかるようなファイルがグレーアウトして投稿対象として選べないようになっています!
ブラウザを通しては悪いことができなくなりました!
##13.4.3 画像のリサイズ##
ファイルサイズの制限の次は画像サイズです
同じように見えますが、ファイルサイズの小さいめちゃめちゃ荒いけども画像サイズが大きい画像ファイルもあり得ます。
そんなものが投稿されるとレイアウトが一気に崩れます。
これを整えるために、今回「ImageMagick」とかいう3rd_Partyのプログラムを使います
さっきから「ActiveStrorage」やら、初登場が続けざまに登場して混乱しますよね。
こちらもインストールして使用します。
$ sudo apt-get -y install imagemagick
あれ。さっきActiveStorageをインストールした時と形式が違いますよね。そりゃ標準搭載と3rd_Partyとで違いますから。
「apt-get」なんてLinuxのコマンドですよ・・・
$ rails active_storage:install
こちらは元々railsに備わっている機能だからこのような形になるのでしょう。きっと。
話を元に戻しましょう。
「ImageMagick」を使うにはgemのインストールが必要です。ここは「Activestorage」にバリデーションの機能を付け加えた時と同じです。
gem 'image_processing', '1.9.3'
gem 'mini_magick', '4.9.5'
gemを入れたら
$ bundle install
これで機能が加わりましたのでさっそく制限の実装にかかります
まずは、画像サイズを自動的に変更する機能を持つ「desplay_image」メソッドを定義します
:
def display_image
image.variant(resize_to_limit: [500, 500])
end
ここでさりげなく付いているvariantメソッドは、「Active Storage」が提供するメソッドで「変換済み画像を作成してくれる機能を持っています。
これで、どんなにでかい画像も幅と高さを500メガピクセルに変換するということに落ち着きました。
これを表示の方のパーシャルにはめ込んで完成です。
<%= image_tag micropost.display_image if micropost.image.attached? %>
今回は、画像サイズのでかいファイルは投稿できないように制限をかけるのではなく、強制的にサイズを変更して表示する、ということですのでバリデーションは必要ありません。。。
##13.4.4 本番環境での画像アップロード##
開発環境ではローカルのファイルシステムに画像を保存するようになっているため、本番環境では動作しません。
Herokuに至っては、一時的にしか画像を保存しないため、デプロイするたびにアップロードした画像が削除される始末・・・
じゃあどこに保存するのよ。ってことで今回はそのためにクラウドを使用します。
AWSのサービスのひとつであるS3を使用します。
ただし!!S3の初期設定はやや高度(超高度)であるみたいで・・・Railsそのものというよりかは、AWSの設定になるので解説はカットします・・・
テキストにもサラッとしか書いておらず、読んでも実際やってみたんですけどうまくいきませんでした。
AWSの設定がどこか違うのでしょうね。。。
#13.5 最後に#
後半かなり乱暴に進めましたが、投稿の章はひとまず完了です。お疲れまさでした
いつものようにgitにpushして、herokuにデプロイしましょう
補足ですが、動画ではherokuにデプロイはともかくgitにpushについてはもっと頻繁にやるべきですみたいなことを言ってました。
$ rails test
$ git add -A
$ git commit -m "Add user microposts"
$ git checkout master
$ git merge user-microposts
$ rails test
$ git push
$ git push heroku
とまあここまではいつも通り。
ちょっと気になったのがこれ
$ heroku pg:reset DATABASE
えええ〜〜とこんなんあったかいな・・・
これを入力するとherokuのURLで「.herokuapp.com/」の前にあるやつ。を入れるように言われるので入れます。
そのあとデータベースをいったんリセットして、seedを流し込みます
$ heroku run rails db:migrate
$ heroku run rails db:seed
この状態で本番環境でサイトを開いてみると・・・
何と11章で悲鳴をあげてた、アカウントを有効化できない問題を解決することができたのです。。。
やはり、問題はデータベースだったみたいですね。
#13章の要約をようやく作り終えての感想#
13章はこれまででやったことがモリモリの盛り沢山でした。
思い出しながらやることでいい復習にもなりました。ただ、自分の悪い癖で時間がかかりすぎました。ひとつひとつを理解することは大事ですが、時間が経つとどうしても忘れてしまうのは避けられないので一気に集中して進めることをお勧めします。
正直章の最後にある「本章のまとめ」を読んで、ほとんどがあれ?これ何だったっけ・・・となりました。さすがに見たら思い出しますが、ポイントはしっかりメモを取っておくことですね。
本番環境への画像投稿については、S3の初期設定がうまくいきませんでしたが、ここはいったん退きます・・・I'll be back!