techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

Rails Tutorial 5 User microposts

Rails Tutorial 5 User microposts

Twitterのようなマイクロポスト機能を実装していく。

A Micropost model

text 型のcontent属性, integer 型のuser_idを持つ Micropost モデルを作成する。

$ rails generate model Micropost content:text user:references
Running via Spring preloader in process 2331
      invoke  active_record
      create    db/migrate/20170514234635_create_microposts.rb
      create    app/models/micropost.rb
      invoke    test_unit
      create      test/models/micropost_test.rb
      create      test/fixtures/microposts.yml

user:references という引数をつけることで、belongs_toのコードが自動追加される。
また、user_idも自動的に追加される。

add_index :microposts, [:user_id, :created_at] を追加してマイグレーションを実行する。

$ rails db:migrate
== 20170514234635 CreateMicroposts: migrating =================================
-- create_table(:microposts)
   -> 0.0086s
-- add_index(:microposts, [:user_id, :created_at])
   -> 0.0018s
== 20170514234635 CreateMicroposts: migrated (0.0108s) ========================

List 13.2 では ApplicationRecord だったものが、List 13.5 では ActiveRecord::Base になっている。

意図したものか、ただの誤植か。

  • モデルのバリデーションのテストを実装
  • バリデーションの追加
    • user_id の存在性確認
    • micropost の存在性確認
    • micropost が 140 文字を超えないように
  • テストが通ることを確認
  • 正しい慣習
    • belongs_to, has_many の関係にあるモデル間では user.microposts.create のように作成できるのが慣習として正しい
    • build メソッドはオブジェクトを返すが DB には反映されない
    • User モデル側に has_many :microposts を追加する。
      • これで(belongs_toはgenerateで追加済みなので) 慣習的に正しい実装が可能になる
  • マイクロポストを作成日時の逆順で取り出す
    • テスト駆動で進める
    • fixture の追加
      • created_at はマジックカラムで手動更新できないが、fixture 内では可能
    • ラムダ式を使った降順取り出し
  • ユーザー削除時にそのユーザーのマイクロポストを同時に削除
    • dependent: :destroy オプションを使って自動的に実行されるようにする
    • 上記のテストを実装する

などを学びながら進める。

Showing microposts

マイクロポストを表示する処理を実装する。
まずはサンプルデータを再生成しておく。

$ rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
== 20161023011602 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0019s
== 20161023011602 CreateUsers: migrated (0.0020s) =============================

== 20161106170056 AddIndexToUsersEmail: migrating =============================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0013s
== 20161106170056 AddIndexToUsersEmail: migrated (0.0016s) ====================

== 20161109220735 AddPasswordToUsers: migrating ===============================
-- add_column(:users, :password_digest, :string)
   -> 0.0007s
== 20161109220735 AddPasswordToUsers: migrated (0.0009s) ======================

== 20170206234706 AddRememberDigestToUsers: migrating =========================
-- add_column(:users, :remember_digest, :string)
   -> 0.0008s
== 20170206234706 AddRememberDigestToUsers: migrated (0.0009s) ================

== 20170425032634 AddAdminToUsers: migrating ==================================
-- add_column(:users, :admin, :boolean, {:default=>false})
   -> 0.0011s
== 20170425032634 AddAdminToUsers: migrated (0.0012s) =========================

== 20170426235417 AddActivationToUsers: migrating =============================
-- add_column(:users, :activation_digest, :string)
   -> 0.0009s
-- add_column(:users, :activated, :boolean, {:default=>false})
   -> 0.0006s
-- add_column(:users, :activated_at, :datetime)
   -> 0.0004s
== 20170426235417 AddActivationToUsers: migrated (0.0022s) ====================

== 20170508234917 AddResetToUsers: migrating ==================================
-- add_column(:users, :reset_digest, :string)
   -> 0.0008s
-- add_column(:users, :reset_sent_at, :datetime)
   -> 0.0004s
== 20170508234917 AddResetToUsers: migrated (0.0013s) =========================

== 20170514234635 CreateMicroposts: migrating =================================
-- create_table(:microposts)
   -> 0.0020s
-- add_index(:microposts, [:user_id, :created_at])
   -> 0.0011s
== 20170514234635 CreateMicroposts: migrated (0.0034s) ========================
$ rails db:seed

次にコントローラとビューを生成する。

$ rails generate controller Microposts
Running via Spring preloader in process 2240
      create  app/controllers/microposts_controller.rb
      invoke  erb
      create    app/views/microposts
      invoke  test_unit
      create    test/controllers/microposts_controller_test.rb
      invoke  helper
      create    app/helpers/microposts_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/microposts.coffee
      invoke    scss
      create      app/assets/stylesheets/microposts.scss
  • 一つのマイクロポストを表示するのには _micropost.html.erb パーシャルを使う
  • User モデルのコンテキストから Micropost のページネーションを行う
    • Userコントローラで show アクションに @microposts 変数を追加する
    • will_paginate の引数に上記 @microposts を指定する
  • 動作確認用にマイクロポストを追加する(seeds.rb)
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin:     true,
             activated: true,
             activated_at: Time.zone.now)

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password,
               activated: true,
               activated_at: Time.zone.now)
end

users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(5)
  users.each { |user| user.microposts.create!(content: content) }
end
  • マイクロポスト用のCSSを追加する
  • プロフィール画面で表示するマイクロポストの統合テストを作成する
    • マイクロポストの fixture ファイルを追加する

Manipulating microposts

Web経由でマイクロポストを作成するためのインターフェースを作成していく。

  • Micropostsコントローラへのアクセス制御でログイン済みか確かめるテストを作成
  • 上記テストが通るように実装を進める
    • logged_in_user メソッドを Users コントローラから Application コントローラに移す
      • Microposts コントローラからも使用できるように
    • Microposts コントローラの before フィルターでアクセス制限をかける
  • テストが通ることを確認する
  • ホーム画面(ルートパス)にフォームを置く
    • ログインしていないユーザーにはサインアップ画面を表示
    • ログイン済みユーザーにはマイクロポスト投稿用フォームを表示
  • ユーザー情報表示はパーシャルで実装
    • pluralize でマイクロポスト数を表示(単数系複数形を正しく表示)
  • マイクロポスト作成フォームもパーシャルで実装
    • homeアクションにマイクロポストのインスタンス変数を追加してパーシャルから参照できるようにする
  • error_messagesパーシャルを修正してUserオブジェクト以外でも使えるようにする
    • 値がオブジェクトで、キーがパーシャルでの変数名と同じハッシュを使うとパーシャルにオブジェクトを渡せる
    • 今までerror_messagesパーシャルを使用していた箇所も上記に合わせて修正する

と進めてマイクロポスト投稿フォームを実装する。
次にHomeにマイクロポストを表示するフィードを実装する。

  • feed は全てのユーザーが持つので User モデルに実装するのが自然
    • 現段階では、現在ログインしているユーザーのマイクロポストを全て取得する
    • ? を使って適切にエスケープさせることで SQLインジェクション を防ぐ
  • ステータスフィードのパーシャルを作成する
    • @feed_itemsの中身はMicropostクラスなので対応する名前のパーシャルを、渡されたリソースのディレクトリ内から探してくれる(?)
    • Homeページにフィードのパーシャルを表示する
  • マイクロポスト投稿失敗時にエラーを起こさないように空の配列を渡しておく

と進める。

次にマイクロポストの削除を実装する。
ログイン済みユーザーが自分のマイクロポストを削除できるようにする。

  • マイクロポスト削除リンクをパーシャルに追加する
  • Microposts コントローラに destroy アクションを追加する
    • before フィルタで find メソッドを使って確認
      • 確認結果NGなら Home ページにリダイレクト
      • OKなら成功メッセージをflashしてリダイレクト
        • リダイレクト先には request.referrer を使って一つ前のURLを指定
          • Homeページ、プロフィールページどちらから削除された場合にも元のページに戻れる
          • || root_urlをデフォルトに設定することで戻るページが見つからなくてもHomeページに戻す

これで削除が動作するようになる。

続いてテストを書いていく。

  • 別々のユーザーに紐付けられたマイクロポストをfixtureに追加
  • 別のユーザーのマイクロポストを削除しようとしてHomeページにリダイレクトされることを確認
  • 統合テストの作成
    • ログインしてマイクロポストのページネーションがされていること
    • 無効(空)なマイクロポストを投稿してエラー表示されること
    • 有効なマイクロポストの投稿
    • 自分のマイクロポストの削除
    • 他人のマイクロポストの削除

以上でマイクロポストの操作については実装完了。

Micropost images

画像付きマイクロポストを投稿できるよう機能追加する。
画像アップローダーには CarrierWave を使用するそうな。

carrierwaveuploader/carrierwave/ Classier solution for file uploads for Rails, Sinatra and other Rub

Classier solution for file uploads for Rails, Sinatra and other Ruby web frameworks

Sinatra でも使えるのか。画像アップロードのデファクト?

Gemfile に追記して bundle install すると rails generate でアップローダーが作れるようになる。

$ rails generate uploader Picture
Running via Spring preloader in process 3920
      create  app/uploaders/picture_uploader.rb

続いてマイクロポストのデータモデルに picture 属性を追加するためのマイグレーションを行う。

$ rails generate migration add_picture_to_microposts picture:string
Running via Spring preloader in process 2324
      invoke  active_record
      create    db/migrate/20170611235521_add_picture_to_microposts.rb
$ rails db:migrate
== 20170611235521 AddPictureToMicroposts: migrating ===========================
-- add_column(:microposts, :picture, :string)
   -> 0.0075s
== 20170611235521 AddPictureToMicroposts: migrated (0.0076s) ==================
  • Micropostモデルへの画像の追加
  • フォームへの画像アップローダーの追加
  • 許可された属性のリストへの picture の追加
  • ビューへの画像表示の追加

と進める。

こちら にある通り、form_for を使う場合には multipart form-data encoding type は Rails によって自動的に付与されるとのこと。

これで画像はアップロードできるようになる。
巨大なファイルや無効なファイルをハンドリングできるようにバリデーションを実装する。

  • ファイル拡張子のホワイトリストの追加
    • CarrierWaveで自動生成したアップローダーに追記
  • ファイルサイズのバリデーションの追加
    • Micropostモデルに実装
  • 送信フォームへの制限を追加
    • jQuery を用いる
    • 警告を無視されたり、POST コマンドを直接実行されたりすると意味がない
    • よってサーバー側でのバリデーションが重要になる

続いて画像のリサイズを実装する。
ファイルサイズは制限できても、画像のサイズが制限できていないのでそこに対応していく。

こちらの注釈に以下の記載がある。

It’s possible to constrain the display size with CSS, but this doesn’t change the image size. In particular, large images would still take a while to load. (You’ve probably visited websites where “small” images seemingly take forever to load. This is why.)

CSS で表示サイズを固定するという方法も考えられるが、これでは実際に読み込む画像のサイズは変わらないため無駄に時間がかかることになるそうな。気をつけよう。

以下の ImageMagick というプログラムを使うそうな。
Convert, Edit, Or Compose Bitmap Images @ ImageMagick

そういえば Redmine でも使ってたのでデファクトっぽい?

ImageMagick と Ruby の繋ぎこみに CarrierWave の MiniMagick という gem を使うと。

  • ImageMagick で画像サイズを変更する
  • Ruby から使えるように MiniMagick を使用する
  • resize_to_limit で指定した大きさ以上の画像は指定サイズに縮小する

と進める。

開発環境はこれでいいが、本番環境でアップロードされた画像は Heroku ストレージ上に置かずクラウドストレージ上に保存する。
Heroku のローカルストレージはデプロイする度にクリアされてしまうんだそうな。

  • fog gem を使う
  • クラウドストレージには S3 を使用

ということでこれもお決まりみたいですね。
クレデンシャルはソースコードに入れずに環境変数で設定、という定石に従って設定。

ここまできたら master にマージして Heroku にデプロイする。
デプロイしたら Heroku の DB をリセットする。

$ heroku pg:reset DATABASE
 !    This version of the API has been Sunset.
 !    Please see https://devcenter.heroku.com/changelog-items/1147 for more information.

。。ん?

Legacy Platform API Brownouts start May 24th _ Heroku Dev Center

あー、そうですか。

$ heroku --version
heroku-toolbelt/3.43.3 (x86_64-linux) ruby/2.3.0
heroku-cli/5.2.20-9d094b0 (linux-amd64) go1.6.2
You have no installed plugins.

古いんですね。アップデート

$ sudo apt-get update; sudo apt-get install heroku-toolbelt heroku
$ heroku --version
heroku-cli/6.11.19-a460a01 (linux-x64) node-v7.10.0

気を取り直して。

$ heroku pg:reset DATABASE
 ▸    WARNING: Destructive action
 ▸    postgresql-adjacent-40581 will lose all of its data
 ▸    
 ▸    To proceed, type fierce-wave-40771 or re-run this command with --confirm
 ▸    fierce-wave-40771

> fierce-wave-40771
Resetting postgresql-adjacent-40581... done

よしよし。

次はいよいよ最終章!

その他

最後にやや焦ったけど通せた通せた。

Rails 5.1 からクレデンシャルをどう扱うか、みたいな考えが変わってたはず。
はよキャッチアップしよ。

Listing 13.22

<li id="micropost-<%= micropost.id %>"> This is a generally good practice, as it opens up the possibility of manipulating individual microposts at a future date (using JavaScript, for example).

だったのが Listing 13.51

<li id="<%= micropost.id %>">

になってるのは間違いかな。質問してみようかな。

こちらの注釈 がなかなか。

To learn how to do things like this, you can do what I did: Google around for things like “javascript maximum file size” until you find something on Stack Overflow.

ま、まぁそうね。WWDC のラボでも同じようなこと言われたしもうこれ定着してるのかな。