やること
ユーザーが短いメッセージを投稿できる機能「マイクロポスト」を追加する
13.1 Micropostモデル
Micropostモデルを作っていく
データ検証とUserモデルの関連付けを含んでいる
13.1.1 基本的なモデル
Micropostモデルはマイクロポストの内容を保存するcontent属性と、特定のユーザーとマイクロポストを結びつけるuser_id属性を持っている
contentはtext型
text型は、投稿フォームにString用のテキストフィールドではなくてText用のテキストエリアを使うため、より自然な投稿フォームが実現できる。いつか国際化をするときに、言語に応じて投稿の長さを調節することもできる
Micropostモデル作成
$ rails generate model Micropost content:text user:references
reference型なので自動的にインデックスと外部キー参照付きのuser_idカラムが追加され、UserとMicropostを関連付けする下準備をしてくれる
add_index :microposts, [:user_id, :created_at]
user_idに関連付けられたすべてのマイクロポストを作成時刻の逆順で取り出しやすくなる
DB更新
$ rails db:migrate
演習
1 nil
2 micropostに代入されているuser_idのuserとその名前が出る
3 保存した日時が入っている
13.1.2 Micropostのバリデーション
基本的なモデルができたので、バリデーションを追加する
Micropostが持つuser_idを使って、監修的に正しくActive Recordの関連付けを実装していく
まずはMicropostモデル単体をテスト駆動開発で動くようにしていく
リスト13.4 micropost.test
setupでfixtureのサンプルユーザーと紐付けたマイクロポスト作成
次にそれが有効かテスト
最後にuser_idがnilのマイクロポストはfalseになるテスト作成
リスト13.5
マイクロポストのuser_idが空でないかのバリデーション追加
次にマイクロポストのcontent属性に対するバリデーション追加
リスト13.7 content属性に対するテスト
空のcontentはfalseになるか
140字を超えるcontentはfalseになるか
リスト13.8 contentに対するバリデーション追加(空でないか、140字を超えていないか)
演習
1 "User must exist", "User can't be blank", "Content can't be blank"
2 ["User must exist", "User can't be blank", "Content is too long (maximum is 140 characters)"]
13.1.3 User/Micropostの関連付け
それぞれのマイクロポストは1人のユーザーと関連づけられる(belongs_to)
それぞれのユーザーは(潜在的に)複数のマイクロポストと関連づけられる(has_many)
これらの関連付けを実装する一環としてMicropostモデルとUserモデルにテスト追加する
これらの関連付けを行うことでユーザーを通してマイクロポストを作成できる
Micropost.create
Micropost.create!
Micropost.new
↓
user.microposts.create
user.microposts.create!
user.microposts.build
user_idが自動的に正しい値になる
リスト13.10 Micropostモデルにbelongs_to関連付け
リスト13.11 Userモデルにhas_many関連付け
リスト13.12 micropost_testのsetupメソッド書き換え
演習
1 contentのuser_idがuserのidと同じマイクロポストが作成される
2 userのマイクロポストを探す、エラー:idを要求される
3 true false
13.1.4 マイクロポストを改良する
ユーザーのマイクロポストを特定の順序で取得できるようにする
ユーザーが削除されたらマイクロポストも削除されるようにする
デフォルトのスコープ
user.micropostsメソッドはデフォルトでは呼び出しの順序が決まっていないので、新しい順に表示されるようにする
これのためにdefault scopeというテクニックを使う
マイクロポストの順序テスト
リスト13.14 DB上の最初のマイクロポストがfixture内のマイクロポスト(most_recent)と同じか検証
fixtureのmicropost(most_recent)とMicropostオブジェクトの1つ目が等しいか
リスト13.15 fixtureにサンプルデータ用意
created_atカラムは本来手動で更新できないが、fixtureファイルの中では更新可
現状、テストは通過しない
default_scopeメソッドでテスト成功させる。これはDBから要素を取得したときの順序を指定するメソッド
特定の順序にする場合、default_scopeの引数にorderを与える
created_atの順にするなら→order(:created_at)
さらに降順にするなら→order(created_at: :desc)
DESCとは、SQLの降順(descending)を指す
昇順(ascending)
リスト13.17 上記コードでMicropostモデル更新
ラムダ式(Stabby lambda)とは
Procやlambda (もしくは無名関数)と呼ばれるオブジェクトを作成する文法を指す
->というラムダ式は、ブロックを引数に取り、Procオブジェクトを返す。このオブジェクトは、callメソッドが呼ばれたとき、ブロック内の処理を評価する。よくわからない
Dependent: destroy
ユーザーが破棄された場合、ユーザーのマイクロポストも一緒に破棄されるべき
→has_manyメソッドにオプションを渡すことで実装
リスト13.19 Userモデルのhas_manyメソッド設定
テスト作成
リスト13.20
ユーザー生成、そのユーザーに紐付いたマイクロポスト作成
その後ユーザーを削除しマイクロポストの数が1減っているか確認
13.2 マイクロポストを表示する
ユーザーのshowページに直接マイクロポストを表示する
13.2.1 マイクロポストの描画
ユーザーのプロフ画面にマイクロポストと投稿の総数を表示させる
DBリセット後再度サンプル生成
$ rails db:migrate:reset
$ rails db:seed
コントローラ生成
$ rails generate controller Microposts
ユーザーごとに全てのマイクロポストを表示する
パーシャルを使って@users変数内の全てのユーザーを表示していたのを参考にする
-
<%= render @users %>
-
<%= render @microposts %>
リスト13.22 1つのマイクロポストを表示するパーシャル生成
app/views/microposts/_micropost.html.erbファイルを作成
CSSのidにマイクロポストのidを割り振っている→一般的に良いとされる慣習らしい
time_ago_in_wordsはどれくらい前投稿したか文字列を返すヘルパーメソッド
一度に全てのマイクロポストが表示される問題を修正する
ページネーションを使う
will_paginateに@microposts変数を渡す必要がある
Usersコントローラでshowアクション定義
リスト13.23 showアクションに@microposts変数追加
@micropostに @userのmicropostsのページネーションの指定ページ(params[:page])を代入する
マイクロポストの投稿数はcountメソッドで表示する
リスト13.24 マイクロポストをshowページに表示
@user.micropostsがあったら表示する
13.2.2 マイクロポストのサンプル
サンプルデータにマイクロポスト追加
リスト 13.25 サンプルデータ改良
takeメソッドを使って最初の6人にマイクロポストを追加する
User.order(:created_at).take(6)
(orderメソッドを経由することで作成された最初の6人を呼び出す)
複数ページにわたって描画したいので50個
内容はFaker gemのLorem.sentenceメソッドでそれっぽく
DBリセット後、再生成
リスト13.26 cssで整える
演習
1 1から6を取り出す
2 takeが要素を配列にして先頭から引数の数だけ返すため不要
13.2.3 プロフィール画面のマイクロポストをテストする
プロフィール画面のマイクロポストが正しく表示されるか統合テストを書く
プロフィール画面が正しく描画されるかのテストは有効化のテストで行っている
統合テスト作成
$ rails generate integration_test users_profile
リスト13.27 マイクロポスト用fixtureファイル
ユーザーに紐づけたマイクロポスト作成
データの準備が完了したのでテストを書いていく
リスト13.28
user_path(@user)にgetのリクエスト
users/showが描画される
titleが@user_nameのタグが存在する
テキストが@user.nameのh1タグが存在する
gravatarクラスを持つimgタグがh1タグの内部に存在する
描画されたページに@userのマイクロポストのcountを文字列にしたものがある
paginationクラスを持つdivタグがある
@user.micropostsのページネーションの1ページ目の配列を1個ずつ取り出してmicropostに代入
このページにmicropost.contentがある
response.bodyとはbodyの内容だけでなくページ内全てのURLが含まれている
演習
2 assert_select 'div.pagination', count: 1
13.3 マイクロポストを操作する
web経由でマイクロポストを操作できるようにする
Micropostsリソースへのインターフェースは、主にプロフィールページとHomeページのコントローラを経由して実行されるので、Micropostsコントローラにはnewやeditのアクションは不要。createとdestroyがあればいい
13.3.1 マイクポストのアクセス制御
Micropostsは関連付けられたユーザーを通してアクセスされるのでcreate、destroyアクションを利用するにはログインしている必要がある
リスト13.31 Micropostsコントローラのテスト
各アクションにリクエスト送信
マイクロポストの数が変化していないか
ログインURLにリダイレクトされているか
ログインの有無を確認するlogged_in_userメソッド
UsersコントローラにあるがMicropostsコントローラでも使いたい
各コントローラが継承するApplicationコントローラに移動(リファクタリング)
リスト13.34 Micropostsコントローラにbeforeフィルター追加
演習
親クラスと子クラスに同じメソッドがあり子クラスのメソッドを書き換えるとオーバーライド(親クラスにあるメソッドを子クラスで再定義することによって、子クラス上で親クラスのメソッドを上書きすること)が発生し、挙動が変わってしまう可能性がある
13.3.2 マイクロポストを作成する
マイクロポスト作成フォームはルートパスに作る
→ユーザーのログイン状態に応じてホーム画面の表示を変更する
リスト13.36 Micropostsコントローラのcreateアクション
外部メソッドのmicropost_paramsでStrong Parametersを使って、マイクロポストのcontentだけをweb経由で変更可能にしている
createメソッドの流れ
@micropostにログイン中のユーザーに紐付いた新しいマイクロポストオブジェクトを返す
@micropostを保存できたら
成功フラッシュメッセージ表示
ルートURLにリダイレクト
保存できなければ
static_pages/homeを描画
リスト13.37 static_pagesのhomeにマイクロポスト作成フォームを作る
userがログインしているかどうかで表示を変える
リスト13.38 13.39 サイドバーのユーザー情報部分と、マイクロポスト投稿フォームのパーシャル追加
↑の投稿フォームを動かすために2箇所変更する
1 関連付けを使い@micropostを定義する
リスト13.40 homeアクションにアクションにマイクロポストのインスタンス変数追加
ユーザーがログインしていればログイン中のユーザーに紐付いたMicropostオブジェクトを@micropostに返す
2 エラーメッセージのパーシャルを再定義する
元々のエラーメッセージのパーシャルでは@user変数を参照していたが今回は@micropost変数を参照したい
→フォーム変数fをf.objectにすることで関連付けられたオブジェクトにアクセスできるようにする
リスト 13.41 パーシャルにオブジェクトを渡すために、値がオブジェクトで、キーがパーシャルでの変数名と同じハッシュを利用。言い換えるとobject: f.objectはerror_messagesパーシャルの中でobjectという変数名を作成する。これを使ってエラーメッセージを更新する
このままではテストは通らない
error_messageパーシャルの他の出現場所でエラーが発生
リスト13.43 13.44 13.45 ユーザー登録、パスワード再設定、ユーザー編集のビューをそれぞれ更新
ユーザー登録とユーザー編集のフォームはパーシャルでまとめているのでユーザーフォームを更新する
演習
パーシャルファイルを2つ作り元のコードをそれぞれコピペ
13.3.3 フィードの原型
homeページにマイクロポストが表示されるようにする
userモデルにfeedメソッド実装
リスト13.46 ログインユーザーの全てのマイクロポストを取得
?があることでSQLクエリに代入する前にidがエスケープされるため、SQLインジェクションと呼ばれる深刻なセキュリティホールを避けることができる
SQLインジェクションとは穴埋めになっているSQL文の穴埋め部分に、作った人が意図しない内容を入れることによって、おかしな動きをさせること。もしくは、それができるようになっている状態
(“user_id = ?”, id)第一引数の?の部分に第二引数のidが入る
リスト13.47 フィードのインスタンス変数追加
@feed_itemsにcurrent_userに紐付いたフィードのページネート代入
リスト13.48 パーシャル追加
<%= render @feed_items %> @feed_itemsにはcurrent_userに紐付いたfeedのpaginate(page: params[:page])が代入されている
def feedの中身は Micropost.where("user_id = ?", id)
つまり@feed_itemsの各要素はMicropostクラスを持っている
よってRailsはMicropostのパーシャル(_micropost.html.erb)を呼び出す
リスト13.49 ステータスフィード追加
演習でパーシャルを作ったのでそちらに追加する
リスト13.50 createアクションに空の@feed_itemsインスタンス変数追加
マイクロポストの投稿失敗に備えて必要なフィード変数をこのブランチで渡しておく
しかしまだページネーションは動かない
Homeページに対応するcontrollerパラメータとaction パラメータを明示的にwill_paginateに渡すことで解決
リスト13.51 コントローラとアクションを明示的に設定する
13.3.4 マイクロポストを削除する
マイクロポストを削除する機能実装
deleteリンクで作動させる
投稿した本人のみが削除できるようにする
リスト13.52 マイクロポストのビュー(パーシャル)に削除リンク追加
リスト13.53 Micropostsコントローラにdestroyアクション追加
correct_userフィルターで削除したいマイクポストがログインしているユーザーのものか確認
request.referrerメソッドで1つ前のURL 無ければルートURLにリダイレクトされる
演習
2 redirect_back(fallback_location: root_url)
redirect_backメソッドで直前のアクションへリダイレクト
fallback_locationオプションで直前のアクションがなければルートURLにリダイレクト
13.3.5 フィード画面のマイクロポストをテストする
Micropostsコントローラの認可をチェックするテストと、統合テストを書いていく
リスト13.54 fixturesのMicropostsファイルにマイクロポストを追加する
リスト13.55 Micropostsコントローラの認可をチェックするテスト
自分以外のユーザーのマイクロポストを削除しようとするとリダイレクトされる
michaelとしてログイン
micropostにfixturesのマイクロポストants(関連づけられた湯ユーザーはmichaelとは別人)を代入
次の動作をしたとき、Micropost.countに違いがない(マイクロポストの数が変わらない)
micropostにdeleteのリクエスト
ルートURLにリダイレクト
次は統合テストを書いていく
$ rails generate integration_test microposts_interface
流れ
- ログイン
- マイクロポストのページ分割の確認
- 無効なマイクロポストを投稿
- 有効なマイクロポストを投稿
- マイクロポストの削除
リスト13.56 マイクロポストのUIに対する統合テスト
setupで@userにmichaelを代入
@userとしてログイン
root_pathにgetリクエスト
class=paginationのdivタグが存在する
#無効な送信
次の動作を行なったとき、Micropost.countに違いがない
microposts_pathにcontentが空の無効なpostのリクエスト
idがerror_explanationのdivタグが存在する
#有効な送信
contentに投稿の条件を満たした言葉を代入
次の動作を行なったとき、Micropost.countが1増えているか
microposts_pathに有効なpostのリクエスト
ルートURLにリダイレクト
指定されたリダイレクト先に移動
表示されたページのHTMLにcontentが含まれている
#投稿を削除する
textが'削除'のaタグが存在する
first_micropostに@userの1ページ目の1番目のマイクロポストを代入
次の動作を行なったとき、Micropost.countが1減っているか
micropost_path(first_micropost)にdeleteのリクエスト
#違うユーザーのプロフィールにアクセス(削除リンクがないことを確認)
user_path(users(:archer))にgetリクエスト
textが'削除'のaタグの数が0
演習2
サイドバーにあるマイクロポストの合計投稿数のテスト
@userとしてログイン
root_pathにgetリクエスト
表示されたページのHTMLの中に"#{@user.microposts.count} microposts"が含まれている(複数形)
#まだマイクロポストを投稿していないユーザー
other_userにusers(:maloly)を代入
other_userとしてログイン
root_pathにgetリクエスト
表示されたページのHTMLの中に"0 microposts"が含まれている(複数形)
other_userに関連付けられたマイクロポスト作成(contentに"A micropost"代入)
root_pathにgetリクエスト
表示されたページのHTMLの中に"1 micropsot"が含まれている(単数形)
13.4 マイクロポストの画像投稿
画像付きのマイクロポストを投稿できるようにする。
開発環境用のβ版→本番環境用の完成版
2つの視覚的要素が必要
画像をアップロードするためのフォーム
投稿された画像そのもの
13.4.1 基本的な画像アップロード
ファイルをアップロードするためにRailsに組み込まれているActive Storageという機能を使う←画像に関連づけるモデルを自由に指定できる。
$ rails active_storage:install Active Storageをアプリに追加
↑を実行すると、添付ファイルの保存に用いるデータモデルを作成するためのデータベースマイグレーションが生成される
$ rails db:migrate 忘れずマイグレーション実行
リスト13.59 has_one_attachedメソッドを使って、Micropostモデルに画像を関連付ける
has_many_attachedメソッドを使うと一つの投稿に複数のファイルを添付できる。
リスト13.60 マイクロポストのcreateフォームに画像アップロードフィールド実装
リスト13.61 Micropostsコントローラを更新
micropostオブジェクトに画像を追加できるようにする
attachメソッドを使う
Micropostsコントローラのcreateアクション内で、アップロードされた画像を@micropostオブジェクトにアタッチ
これを許可するためにmicropost_paramsメソッドを更新して:imageを許可済み属性リストに追加
Web経由で更新できるように
リスト13.62 マイクロポスト画像を描画する
image_tagヘルパーで関連づけられたmicropost.imageを描画
attached?論理値でテキストのみのマイクロポストでは画像を表示させないようにする
演習
2. "micropost interface"を更新
input[type=file]のタグがあるか
imageにfixture画像ファイル代入
有効なマイクロポスト送信
image属性は有効か
13.4.2 画像の検証
現状アップロードされた画像に制限がないので大きな画像も無効なファイルも上げられる
→画像サイズやフォーマットに対するバリデーションを実装する
リスト13.65 Active Storageバリデーションgem追加
$ bundle install
リスト13.66 画像バリデーション追加
%w[]は配列構築文
バリデーション強化のためブラウザ側にもチェックする仕組みを作る
リスト13.67 jQueryでファイルサイズ確認
CSS idのmicropost_imageを含んだ要素を見つけ出し、監視
マイクロポストのフォームを指している
このCSS idを持つ要素が変化したとき、このjQueryの関数が動く
そして、もしファイルサイズが大きすぎた場合、alertメソッドで警告を出す
リスト13.68 フォーマット確認
acceptを用いて有効なフォーマットのみ受け入れる
13.4.3 画像のリサイズ
ファイルのサイズに対するバリデーションのほかに画像の大きさに制限を設けていく
(CSSでサイズを調整する方法もあるが、これではファイルサイズは変わらない)
画像を操作するプログラム"imageMagick"導入
クラウドIDEの場合
$ sudo apt-get -y install imagemagick
リスト13.69 gem追加
$ bundle install
リスト13.70 リサイズした画像を返すメソッド
variantメソッドで変換画像を作成
resize_to_limitでピクセルに制限をかける
リスト13.71
これでdisplay_imageが使えるようになった
大規模なサイトではバックグラウンドでバックグラウンドで処理する
詳しくはActive jobを調べる
13.4.4 本番環境での画像アップロード
一旦スキップ
13.5 最後に
$ rails test
$ git add -A
$ git commit -m "Add user microposts"
$ git checkout master
$ git merge user-microposts
$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed
※heroku login時、うまくいかなければ
$ heroku login --interactive