『Ruby on Rails チュートリアル』学習メモ
Ch01 0からdeployまで
- お手軽すぎるscaffold
- 自動生成されたcodeは無駄に量が多く複雑 → 初学者には向かない
- rails 5.1.6 new hello_app ← バージョン指定
- direcroty 構成 → 表1.2
- Gemfileのバージョン指定
- ~>では,メジャーバージョンのアップデートはしない
- list 1.5でGemfileの内容
- root 'controller_name#action_name'
- Git
- git config --global user.name "自分の名前"
- git config --global user.email your.email@example.com
- git config --global alias.co checkout
- git config --global credential.helper "cache --timeout=86400"
- git init
- git add -A
- .gitignoreで指定していないすべてのファイルをバージョン管理に追加
- git commit -m "Initialize repository"
- git log
- git checkout -f
- 以前のcommitに復元できる
- git remote add origin https://github.com/<あなたのGitHubアカウント名>/hello_app.git
- git push -u origin master
- git checkout -b modify-README
- git branch
- github.com/settings/repositoriesをブラウザで開いて「Repository default branch」をmainからmasterに変更
- 編集後に,git statusで確認
- git commit -a -m "Improve the README file"
- -a: 変更を一括でcommit
- 慎重に使う
- 新しいファイルはaddが必要
- -a: 変更を一括でcommit
- commit message: 現在形かつ命令形
- git checkout master
- git merge modify-README
- git branch -d modify-README
- git branch -D
- topic branch上の変更を破棄
- 変更をマージしていなくても削除できる
- git push
- deploy
- Herokuのセットアップ
- PostgreSQL
- group :production do gem 'pg', '0.20.0' end
- bundle install --without production
- pg gemを追加したことをGemfile.lockに反映させないと、本番環境へのデプロイで失敗してしまう
- git commit -a -m "Update Gemfile for Heroku"
- curl https://cli-assets.heroku.com/install.sh | sh
- heroku --version
- heroku login --interactive
- heroku keys:add
- heroku create
- git push heroku master
- heroku open
- heroku rename rails-tutorial-hello
- heroku logs
- Herokuのセットアップ
- まとめ
- Ruby on Rails: Web developmentのためのframework, Rubyによる
- rails new, rails server
- controller_actionの追加やroot routingの変更だけで,hello, world applicationを作成できる
- Herokuへのdeploy
Ch02 Toy application
- application plan
- 下準備: Ch01と同じところまで
- 最初に,applicationの構造を表すためのdata_modelを作成する
- userのdata_model design
- minimum 表現
- id, name, email
- minimum 表現
- micro_postのdata_model design
- id, content, user_id(associate)
- Users resource
- resource: data_modelとWeb_interfaceの組み合わさり
- CRUDできるobjectとみなせるようになる
- rails generate scaffold User name:string email:string
- rails db:migrate
- かつてはRake
- ユーザーページを探検する
- /users: index
- /users/1: show
- /users/new: new
- /users/1/edit: edit
- MVCの挙動
- config/routes.rb
- URL, actionの組み合わせを効率よく設定
- resources :users
- :usersはRubyのsymbol
- app/controllers/users_controller.rb
- 各ページは、Usersコントローラ内のアクションにそれぞれ対応
- ページの数よりもアクションの数の方が多い
- create、update、destroyアクションがあります。通常、これらのアクションは、ページを出力せずにデータベース上のユーザー情報を操作
- URLには重複しているものがある
- これらのアクション同士の違いは、それらのアクションに対応するHTTP requestメソッドの違い
- REST
- RailsアプリケーションにおけるRESTとは、アプリケーションを構成するコンポーネント (ユーザーやマイクロポストなど) を「リソース」としてモデル化することを指す
- これらのリソースは、リレーショナルデータベースの作成/取得/更新/削除 (Create/Read/Update/Delete: CRUD) 操作と、4つの基本的なHTTP requestメソッド (POST/GET/PATCH/DELETE) の両方に対応
- RESTfulなスタイルを採用することで、作成すべきコントローラやアクションの決定が楽になる。作成(C)・取得(R)・更新(U)・削除(D)を行うリソースだけでアプリケーション全体を構成してしまうことも可能
- RailsアプリケーションにおけるRESTとは、アプリケーションを構成するコンポーネント (ユーザーやマイクロポストなど) を「リソース」としてモデル化することを指す
- @記号で始まる変数をRubyではインスタンス変数と呼び、Railsのコントローラ内で宣言したインスタンス変数はビューでも使えるようになる
- app/models/user.rb
- ビューはその内容をHTMLに変換し (⑦)、コントローラがブラウザにHTMLを送信して、ブラウザでHTMLが表示
- config/routes.rb
- Usersリソースの欠点
- データの検証が行われていない
- ユーザー認証が行われていない
- テストが書かれていない
- レイアウトやスタイルが整っていない
- 理解が困難
- resource: data_modelとWeb_interfaceの組み合わさり
- Micropostsリソース
- マイクロポストを探検する
- rails generate scaffold Micropost content:text user_id:integer
- マイクロポストをマイクロにする
- app/models/micropost.rb
- validates :content, length: { maximum: 140 }
- app/models/micropost.rb
- ユーザーはたくさんマイクロポストを持っている
- 異なるデータモデル同士の関連付けは、Railsの強力な機能
- app/models/user.rb
- has_many :microposts
- app/models/micropost.rb
- belongs_to :user
- 演習
- <%= @user.microposts.first.content %>
- app/models/micropost.rb
- presence: true
- app/models/user.rb
- validates :name, presence: true
- 継承の階層
- ActiveRecord::Baseという基本クラスを継承したことによって、作成したモデルオブジェクトはデータベースにアクセスできるようになり、データベースのカラムをあたかもRubyの属性のように扱えるようになる
- ActionController::Base
- アプリケーションをデプロイする
- マイクロポストを探検する
- 最後に
- 良い
- 弱点
- レイアウトもスタイルも設定されていない
- “Home” や “About” のような定番の静的なページがない
- ユーザーがパスワードを設定できない
- ユーザーが画像を置けない
- ログインの仕組みがない
- セキュリティのための仕組みがまったくない
- ユーザーとマイクロポストの自動関連付けが行われていない
- Twitterのような「フォロワー (following)機能」や「フォロー中 (followed)機能」がない
- マイクロポストをフィードできない
- まともなテストがない
- 理解が困難
- 本章のまとめ
- Scaffold機能でコードを自動生成すると、Webのあらゆる部分からモデルデータにアクセスしてやりとりできるようになる
- Scaffoldは何よりも手っ取り早いのがとりえだが、これを元にRailsを理解するには向いていない
- RailsではWebアプリケーションの構成にMVC (Model-View-Controller) というモデルを採用している
- Railsが解釈するRESTには、標準的なURLセットと、データモデルとやりとりするためのコントローラアクションが含まれている
- Railsではデータのバリデーション (validation) がサポートされており、データモデルの属性の値に制限をかけることができる
- Railsには、さまざまなデータモデル同士を関連付けを定義するための組み込み関数が多数用意されている
- Railsコンソールを使うと、コマンドラインからRailsアプリケーションとやりとりすることができる
Ch03 ほぼ静的なページの作成
- 静的なページを自分の手で作成することは良い経験になり、多くの示唆も得られる
- セットアップ
- rails 5.1.6 new sample_app
- Gemfile
- git
- README
- helloアクションをApplicationコントローラーに追加
- ルートルーティングを設定
- Herokuにプッシュ
- RailsのデフォルトのページはHeroku上ではうまく表示されない仕様になっています。このため、hello worldのような変更を加えないとデプロイが成功したかどうかが分かりづらい、というのが本当の理由
- heroku logsでerrorの確認
- 静的ページ
- コントローラとは (基本的に動的な) Webページの集合を束ねるコンテナのこと
- git checkout -b static-pages
- 静的なページの生成
- rails generate controller StaticPages home help
- git push -u origin static-pages
- 元に戻す方法
- GETやその他のHTTPメソッドについて
- StaticPagesコントローラは一般的なRESTアクションに対応していないことに注意してください。これは、静的なページの集合に対しては、適切なアクション
- ApplicationControllerクラスを継承しているため、StaticPagesControllerのメソッドは (たとえ何も書かれていなくても) Rails特有の振る舞い
- /static_pages/homeというURLにアクセスすると、RailsはStaticPagesコントローラを参照し、homeアクションに記述されているコードを実行
- 今回の場合、homeアクションが空になっているので、/static_pages/homeにアクセスしても、単に対応するビューが出力されるだけ
- /static_pages/homeというURLにアクセスすると、RailsはStaticPagesコントローラを参照し、homeアクションに記述されているコードを実行
- 静的なページの調整
- テストから始める
- 何らかの変更を行う際には、常に「自動化テスト」を作成して、機能が正しく実装されたことを確認する習慣をぜひ身に付けましょう
- 結局テストはいつ行えばよいのか
- テストを行う目的
- テストの手法やタイミングは、ある意味テストをどのぐらいすらすら書けるかで決まると言ってよい
- 「テスト駆動」にするか「一括テスト」にするかを決める目安となるガイドライン
- アプリケーションのコードよりも明らかにテストコードの方が短くシンプルになる (=簡単に書ける) のであれば、「先に」書く
- 動作の仕様がまだ固まりきっていない場合、アプリケーションのコードを先に書き、期待する動作を「後で」書く
- セキュリティが重要な課題またはセキュリティ周りのエラーが発生した場合、テストを「先に」書く
- バグを見つけたら、そのバグを再現するテストを「先に」書き、回帰バグを防ぐ体制を整えてから修正に取りかかる
- すぐにまた変更しそうなコード (HTML構造の細部など) に対するテストは「後で」書く
- リファクタリングするときは「先に」テストを書く
- 特に、エラーを起こしそうなコードや止まってしまいそうなコードを集中的にテスト
- 不安定な要素が特に見当たらないアプリケーションや、(主にビューが) 頻繁に改定される可能性の高いアプリケーションのコードを書くときには、思い切ってテストを省略してしまうこともある
- 本書における主要なテストは、コントローラテスト (この節より)、モデルテスト (第6章より)、統合テスト (第7章より) の3つ
- 統合テストでは、ユーザーがWebブラウザでアプリケーションとやりとりする操作をシミュレートできるので特に強力
- まずは取っ付きやすいコントローラテストから
- 最初のテスト
- Red
- Green
- Refactor
- 少しだけ動的なページ
- ページの内容に応じて、ページのタイトルを自ら書き換えて表示
- タイトルをテストする (Red)
- 典型的なWebページは、リスト 3.23のようなHTMLの構造
- 1) document type (doctype)は使用するHTMLのバージョン (ここではHTML5) をブラウザに対して宣言
- 2) headセクション。ここではtitleタグに囲まれた「Greeting」(=あいさつ) という文字
- 3) bodyセクション。ここには「Hello, world!」という文字列
- セレクタ
- assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
- 典型的なWebページは、リスト 3.23のようなHTMLの構造
- タイトルを追加する (Green)
- レイアウトと埋め込みRuby (Refactor)
- 重複を取り除くテクニックの1つとして、ビューで「埋め込みRuby」(Embedded Ruby) が使えます
- Railsのprovideメソッドを使ってタイトルをページごとに変更
- ERBと呼ばれている、Rubyの埋め込みコード
- ERBはWebページに動的な要素を加えるときに使うテンプレートシステム
- <% provide(:title, "Home") %>
- <% ... %>という記法が使われており、その中からRailsのprovideメソッドを呼び出し
- "Home"という文字列と:titleというラベルを関連付け
- <% ... %>と書くと、中に書かれたコードを単に実行するだけで何も出力しません。<%= ... %>のように等号を追加すると、中のコードの実行結果がテンプレートのその部分に挿入
- HTMLの重複した構造をDRYにする
- Railsにはそのためにapplication.html.erbという名前のレイアウトファイルがあります
- <%= yield %>
- レイアウトを使う際に、/static_pages/homeにアクセスするとhome.html.erbの内容がHTMLに変換され、<%= yield %>の位置に挿入
- スタイルシートとJavaScriptは、Asset Pipeline (5.2.1) の一部
- csrf_meta_tagsは、Web攻撃手法の1つであるクロスサイトリクエストフォージェリー (Cross-Site Request Forgery: CSRF)を防ぐために使われるRailsのメソッド
- レイアウトと重複するHTMLがまだ残っているので、それらを削除して、内部のコンテンツだけ残します
- 演習
- まずはテストを追加
- urlを変えるのも必要
- routesを追加
- controller_actionを追加
- erbを追加
- まずはテストを追加
- ルーティングの設定
- 演習
- root_url
- 演習
- 最後に
- 本章のまとめ
- 新しいRailsアプリケーションをゼロから作成したのはこれで3度目。今回も必要なgemのインストール、リモートリポジトリへのプッシュ、production環境まで行った
- コントローラを新規作成するためのrailsコマンドはrails generate controller ControllerName アクション名 (省略可)
- 新しいルーティングはconfig/routes.rbファイルで定義
- Railsのビューでは、静的HTMLの他にERB (埋め込みRuby: Embedded RuBy) が使える
- 常に自動化テストを使って新機能開発を進めることで、自信を持ってリファクタリングできるようになり、回帰バグも素早くキャッチ
- テスト駆動開発では「red ・ green ・REFACTOR」サイクルを繰り返す
- Railsのレイアウトでは、アプリケーションのページの共通部分をテンプレートに置くことでコードの重複を解決
- 本章のまとめ
- 高度なセットアップ
- minitest reporters
- minitest-reporters gemを利用
- test/test_helper.rb
- require "minitest/reporters"
- Minitest::Reporters.use!
- test/test_helper.rb
- minitest-reporters gemを利用
- Guardによるテストの自動化
- テストをしようとする度にエディタからコマンドラインに移動して、手動でコマンドを打ち込み、実行しなければならないという点が面倒
- Guardは、ファイルシステムの変更を監視
- 具体的には、「home.html.erbファイルが変更されたらstatic_pages_controller_test.rbを自動的に実行する」
- bundle exec guard init
- guard :minitest, spring: "bin/rails test", all_on_start: false do
- GuardからSpringサーバーを使って読み込み時間を短縮しています (SpringはRailsの機能の1つです)
- 開始時にテストスイートをフルで実行しないようGuardに指示
- .gitignoreファイルにspring/ディレクトリを追加
- Springのプロセスが起動したまま多数残留すると、テストのパフォーマンスが低下
- bundle exec guard
- テストを変更ファイルだけではなく、フルで実行したい場合は、guard>プロンプトでReturnキーを押します
- (このとき、Springサーバーに接続できないなどのエラーが表示されることがあります。問題を修正するには、もう一度Returnキーを押します)
- Guardを終了するにはCtrl-Dキー
- minitest reporters
Ch04 Rails風味のRuby
- Railsにおいて重要となるRubyのさまざまな要素について探っていく
- Rails開発者にとって必要な知識は比較的少なくて済みます
- この章の目的は、「Rails風味のRuby」というものについての確固たる基盤を、皆さんのこれまでの言語経験に関わらず提供すること
- 動機
- 初歩的な作業をいつまでも続けるわけにはいきません
- Rubyに関する知識と経験の限界に真正面から挑み、これを乗り越えるためにこの章を割り当てる
- 組み込みヘルパー
- カスタムヘルパー
- app/helpers/application_helper.rb
- 文字列とメソッド
- Railsコンソールはirb (IRB: Interactive RuBy) を拡張して作られているため、Rubyの機能をすべて使うことができます
- rails console
- Railsコンソールは素晴しい学習ツールであり、その中を自由に探索できます
- コンソールの中で何をしようとも、何かを壊すことは (まず) ありえないので、ご安心ください
- Railsコンソールが何かおかしな挙動になったら、Ctrl-Cを押してコンソールから強制的に抜け出すことができます。もしくは、Ctrl-Dを押して正常にコンソールを終了させることもできます
- Rubyリファレンスマニュアル (通称: るりま) やるりまサーチが大変便利
- コメント
- Rubyのコメントはナンバー記号# (「ハッシュマーク」や「オクトソープ」とも呼ばれます) から始まり、その行の終わりまでがコメント
- 文字列
- 文字列 (string) は、Webアプリケーションにおいておそらく最も重要なデータ構造
- Webページというものが究極的にはサーバーからブラウザに送信された文字列にすぎないため
- 文字列リテラルと呼ばれ (面白いことにリテラル文字列とも呼ばれます)ダブルクォート " で囲むことで作成
- 演算子を使って、文字列を結合
- 文字列を組み立てる他の方法として式展開 (interpolation) というものがあり、#{}という特殊な構文を使います
- 出力
- puts (putの三人称単数現在形ではなく「put string」の略なので、本来は「put ess」と発音しますが、最近は「puts」と発音されることも多くなってきました)
- putsを使って出力すると、改行文字である\nが自動的に出力の末尾に追加されます (これはターミナルのechoコマンドと同じ振る舞いになります)
- printメソッドも同様の出力を行いますが、次のように、改行文字を追加しない点が異なります
- シングルクォート内の文字列
- 演習
- tab: \t
- 文字列 (string) は、Webアプリケーションにおいておそらく最も重要なデータ構造
- オブジェクトとメッセージ受け渡し
- オブジェクトとは (いついかなる場合にも) メッセージに応答するもの
- オブジェクトに渡されるメッセージは、一般にはメソッドと呼ばれます。メソッドの実体は、そのオブジェクト内で定義されたメソッド
- メソッドがtrueまたはfalseという論理値 (boolean) を返すことを、末尾の疑問符で示す慣習
- 論理値は、特に処理の流れを変更するときに有用
- Rubyでは、あらゆるものがオブジェクトです。したがって、nilもオブジェクト
- puts "x is not empty" if !x.empty?
- Rubyではこのように、後続するifでの条件式が真のときにだけ実行される式 (後続if) を書くことができ、コードが非常に簡潔になります。なお、unlessキーワードも同様に使えます
- Rubyにおいてnilは特別なオブジェクト
- オブジェクトとは (いついかなる場合にも) メッセージに応答するもの
- メソッドの定義
- メソッドの引数を省略することも可能です (カッコですら省略可能です)
- 引数にデフォルト値を含めているから
- Rubyのメソッドには「暗黙の戻り値がある」ことにご注意ください。これは、メソッド内で最後に評価された式の値が自動的に返される
- メソッドの引数を省略することも可能です (カッコですら省略可能です)
- titleヘルパー、再び
- module ApplicationHelperという要素について
- モジュールは、関連したメソッドをまとめる方法の1つで、includeメソッドを使ってモジュールを読み込むことができます (ミックスイン (mixed in) とも呼びます)
- Railsでは自動的にヘルパーモジュールを読み込んでくれるので、include行をわざわざ書く必要がありません
- module ApplicationHelperという要素について
- ほかのdata_structure
- 配列と範囲演算子
- "fooxbarxbaz".split('x')
- Rubyでは、角カッコ以外にも配列の要素にアクセスする方法が提供されています
- first, second, last
- a.last == a[-1]
- true
- 配列の内容を変更したい場合は、そのメソッドに対応する「破壊的」メソッドを使います。破壊的メソッドの名前には、元のメソッドの末尾に「!」を追加したものを使うのがRubyの慣習です
- pushメソッド (または同等の<<演算子) を使って配列に要素を追加することもできます
- 他の多くの言語の配列と異なり、Rubyでは異なる型が配列の中で共存できます
- 文字列を配列に変換するのにsplitを使いました。joinメソッドはこれと逆の動作
- a.join(', ')
- 範囲 (range) は、配列と密接に関係しています。to_aメソッドを使って配列に変換すると理解しやすい
- (0..9).to_a
- 範囲は、配列の要素を取り出すのに便利
- a[0..2]
- インデックスに-1という値を指定できるのは極めて便利です。-1を使うと、配列の長さを知らなくても配列の最後の要素を指定することができ、これにより配列を特定の開始位置の要素から最後の要素までを一度に選択することができます
- a[2..-1]
- 文字列に対しても範囲オブジェクトが使えます
- ('a'..'e').to_a
- ブロック
- 配列と範囲はいずれも、ブロックを伴うさまざまなメソッドに対して応答することができます
- (1..5).each { |i| puts 2 * i }
- メソッドに渡されている{ |i| puts 2 * i }が、ブロックと呼ばれる部分
- |i|では変数名が縦棒「|」に囲まれていますが、これはブロック変数に対して使うRubyの構文で、ブロックを操作するときに使う変数を指定
- ブロックであることを示すには波カッコ で囲みますが、次のようにdoとendで囲んで示すこともできます
- 短い1行のブロックには波カッコを使い、長い1行や複数行のブロックにはdo..end記法
- ブロックは見た目に反して奥が深く、ブロックを十分に理解するためには相当なプログラミング経験が必要
- (1..5).each { |i| puts 2 * i }
- mapメソッドなどを使ったブロックの使用例
- 3.times { puts "Betelgeuse!" }
- (1..5).map { |i| i**2 }
- %w[a b c].map { |char| char.upcase }
- mapのブロック内で宣言した引数 (char) に対してメソッドを呼び出しています。こういったケースでは省略記法が一般的で、次のように書くこともできます (この記法を“symbol-to-proc”と呼びます)
- %w[A B C].map(&:downcase)
- 元々Ruby on Rails独自の記法でした。しかし多くの人がこの記法を好むようになったので、今ではRubyのコア機能として導入
- mapメソッドは、渡されたブロックを配列や範囲オブジェクトの各要素に対して適用し、その結果を返します
- 最後のブロックの例として、単体テスト
- ('a'..'z').to_a.shuffle[0..7].join
- 演習
- (0..16).map {|i| i**2}
- str.map {|s| s.upcase}.join
- s.split('').shuffle.join
- 配列と範囲はいずれも、ブロックを伴うさまざまなメソッドに対して応答することができます
- ハッシュとシンボル
- ハッシュは本質的には配列と同じですが、インデックスとして整数値以外のものも使える点が配列と異なります (この理由から、Perlなどのいくつかの言語ではハッシュを連想配列と呼ぶこともあります)
- ハッシュのインデックス (キーと呼ぶのが普通です) は、通常何らかのオブジェクト
- ハッシュの波カッコは、ブロックの波カッコとはまったく別物
- {"last_name"=>"Hartl", "first_name"=>"Michael"}
- ハッシュでは要素の「並び順」が保証されない
- ハッシュの1要素を角カッコを使って定義する代わりに、次のようにキーと値をハッシュロケットと呼ばれる=> によってリテラル表現するほうが簡単
- user = { "first_name" => "Michael", "last_name" => "Hartl" }
- key: Railsでは文字列よりもシンボルを使う方が普通
- シンボルは文字列と似ていますが、クォートで囲む代わりにコロンが前に置かれている
- 余計なことを一切考えずに、シンボルを単なる文字列とみなしても構いません
- 余計なものを削ぎ落した結果、シンボル同士の比較を容易に行えます。文字列は1文字ずつ比較する必要がありますが、シンボルは一度に全体を比較できます。これはハッシュのキーとして理想的な性質です。
- シンボルは、Ruby以外ではごく一部の言語にしか採用されていない特殊なデータ形式
- 文字列と違って、全ての文字が使えるわけではない
- 未定義のハッシュ値は単純にnil
- ハッシュではシンボルをキーとして使うことが一般的なので、Ruby 1.9からこのような特殊な場合のための新しい記法がサポート
- h2 = { name: "Michael Hartl", email: "michael@example.com" }
- :name =>とname:は、ハッシュとしてのデータ構造は全く同じ
- ハッシュの値にはほぼ何でも使うことができ、他のハッシュを使うことすらできます → ネストされたハッシュ
- 配列や範囲オブジェクトと同様、ハッシュもeachメソッドに応答します
- 便利なinspectメソッドを紹介します。これは要求されたオブジェクトを表現する文字列を返します
- オブジェクトを表示するためにinspectを使うことは非常によくあることなので、 pメソッドというショートカットがあります
- p :name # 'puts :name.inspect' と同じ
- 演習
- ハッシュは本質的には配列と同じですが、インデックスとして整数値以外のものも使える点が配列と異なります (この理由から、Perlなどのいくつかの言語ではハッシュを連想配列と呼ぶこともあります)
- CSS、再び
- 配列と範囲演算子
- Rubyにおけるクラス
- 最後に
- 本章のまとめ
- Rubyは文字列を扱うためのメソッドを多数持っている
- Rubyの世界では、すべてがオブジェクトである
- Rubyではdefというキーワードを使ってメソッドを定義する
- Rubyではclassというキーワードを使ってクラスを定義する
- Railsのビューでは、静的HTMLの他にERB (埋め込みRuby: Embedded RuBy) も使える
- Rubyの組み込みクラスには配列、範囲、ハッシュなどがある
- Rubyのブロックは (他の似た機能と比べ) 柔軟な機能で、添え字を使ったデータ構造よりも自然にイテレーションができる
- シンボルとはラベルである。追加的な構造を持たない (代入などができない) 文字列みたいなもの
- Rubyではクラスを継承できる
- Rubyでは組み込みクラスですら内部を見たり修正したりできる
- 本章のまとめ
Ch05 レイアウトを作成する
- この章では、アプリケーションにBootstrapフレームワークを組み込み、そして、カスタムスタイルを追加します
- 構造を追加する
- カスタムCSSルールの他に、Twitter社によるオープンソースのWebデザインフレームワークとして公開しているBootstrapも利用します
- コードそのものにもスタイルを与えます。つまり、散らかり始めたレイアウトのコードを、パーシャル (Partial) 機能を使って整えていくということ
- Webアプリケーションを作成するときに、ユーザーインターフェイスの概要をできるだけ早いうちに把握しておくことがしばしば有用
- ナビゲーション
- RailsはデフォルトでHTML5を使います
- 一部のブラウザ (特に旧式のInternet Explorer) ではHTML5のサポートが不完全である場合があります。そのため、次のようなJavaScriptのコード (通称: HTML5 shim (or shiv))3 を使ってこの問題を回避
- Internet Explorerで特別にサポート
- 一部のブラウザ (特に旧式のInternet Explorer) ではHTML5のサポートが不完全である場合があります。そのため、次のようなJavaScriptのコード (通称: HTML5 shim (or shiv))3 を使ってこの問題を回避
- headerタグは、ページの上部に来るべき要素を表します
- すべてのHTML要素には、クラスとidの両方を指定することができます
- これらは単なるラベルで、CSSでスタイルを指定するときに便利
- クラスとidの主な違いは、クラスはページ内で何度でも使えるのに対し、idは一度しか使えない点
- divタグは一般的な表示領域を表し、要素を別々のパーツに分けるときに使われます。特に古いスタイルのHTMLでは、divタグはサイト内のほぼすべての領域で使われていました
- HTML5からはよく使われる領域ごとに細分化できるようになり、具体的にはheader要素、nav要素、section要素が新たに使えるようになりました
- リンクを生成するために、Railsヘルパーのlink_to
- リストアイテムタグliと順不同リストタグulによって作られた、ナビゲーションリンクのリスト
- navタグには「その内側がナビゲーションリンクである」という意図を明示的に伝える役割
- yieldメソッドはWebサイトのレイアウトにページごとの内容を挿入
- 今後のスタイル要素を利用するために、home.html.erbビューに特別な要素をいくつか追加
- 第7章でサイトにユーザーを追加するときに備えて、最初のlink_toで次のような仮のリンクを生成
- divタグのCSSクラスjumbotronや、signupボタンのbtnクラス、btn-lgクラス、btn-primaryクラスはすべて、Bootstrapにおいて特別な意味
- image_tagヘルパーの能力が示されています。このヘルパーでは、シンボルを使ってalt属性などを設定できます
- BootstrapとカスタムCSS
- 多くのHTML要素にCSSクラスを関連付けました。こうしておくことで、CSSベースでレイアウトを構成する際に高い柔軟性
- これらのクラスの多くは、Twitterが作成したフレームワークであるBootstrap特有のもの
- Bootstrapを使うと、洗練されたWebデザインとユーザーインターフェイス要素を簡単にHTML5アプリケーションに追加することができます
- Bootstrapを使うことでアプリケーションをレスポンシブデザイン (Responsive Design) にできるということです。これにより、どの端末でアプリケーションを閲覧しても、ある程度見栄えをよくすることができます
- bootstrap-sass gemを使ってRailsアプリケーションに導入できます
- カスタムCSSを動かすための最初の一歩は、カスタムCSSファイルを作ること
- Bootstrap CSSを追加する
- app/assets/stylesheets/custom.scss
- @import "bootstrap-sprockets";
- @import "bootstrap";
- app/assets/stylesheets/custom.scss
- すべてのページに適用される共通のスタイルをCSSに追加する
- CSSルールでは一般に、クラス、id、HTMLタグ、またはそれらの組み合わせ、のいずれかを指定します。そしてその後ろにスタイリングコマンドのリストを記述します。
- .center冒頭のドット.は、このルールがクラスに対してスタイルを適用することを示しています
- centerクラスに属している (divなどの) タグの内側にある要素が、すべて中央揃えになることを意味
- 冒頭がポンド記号#で始まる場合は、そのルールがCSSのidに対してスタイルを適用することを示します
- Bootstrapには洗練されたタイポグラフィーを利用できるCSSルールがあります
- テキストの見栄えを変えてみましょう
- いくつかのルールをサイトロゴに追加
- テキストを大文字に変換し、サイズ、色、配置を変更
- 演習
- comment out
- <%#= image_tag("kitten.jpg", alt: "Kitten") %>
- すべての画像を非表示
- img {
- display: none;
- img {
- comment out
- 多くのHTML要素にCSSクラスを関連付けました。こうしておくことで、CSSベースでレイアウトを構成する際に高い柔軟性
- RailsはデフォルトでHTML5を使います
- パーシャル (partial)
- app/views/layouts/application.html.erb
- Sassとアセットパイプライン
- CSS、JavaScript、画像などの静的コンテンツの生産性と管理を大幅に強化する「アセットパイプライン (Asset Pipeline)」
- アセットパイプラインの概要と、素晴らしいCSS生成ツールである「Sass」の使い方について
- アセットパイプライン
- Rails開発者の視点からは、アセットディレクトリ、マニフェストファイル、プリプロセッサエンジンという、3つの主要な機能が理解の対象
- アセットディレクトリ
- マニフェストファイル
- プリプロセッサエンジン
- 本番環境での効率性
- Asset Pipelineの最大のメリットの1つは、本番のアプリケーションで効率的になるように最適化されたアセットも自動的に生成されること
- 具体的には、Asset Pipelineがすべてのスタイルシートを1つのCSSファイル (application.css) にまとめ、すべてのJavaScriptファイルを1つのJSファイル (javascripts.js) にまとめてくれます。さらに、それらのファイルすべてに対して 不要な空白やインデントを取り除く処理を行い、ファイルサイズを最小化してくれます。
- 結果として、開発環境と本番環境という、2つの異なった状況に対してそれぞれ最高の環境を提供
- 素晴らしい構文を備えたスタイルシート
- Sass は、スタイルシートを記述するための言語であり、CSSに比べて多くの点が強化
- SCSSは厳密な意味で、CSS本体を抽象化したフォーマットです。つまり、SCSSはCSSに新しい機能を追加しただけで、全く新しい構文を定義したようなものではない
- Railsのアセットパイプラインは、.scssという拡張子を持つファイルをSassを使って自動的に処理
- ネスト
- Sassは、SCSSをCSSに変換する際に、&:hoverを#logo:hoverに置き換え
- 変数
- $light-gray: #777;
- 定義されている変数はBootstrapページの「LESS変数一覧」で参照
- このWebサイトでは、SassではなくLESSを使って変数が定義されていますが、bootstrap-sassというgemを使えば、Sassでも同様の変数が使えるようになります
- リスト 5.20: ネストや変数を使って初期のSCSSファイルを書き直した結果
- レイアウトのリンク
- 名前付きルート
- サイトリンクのルーティングとURLのマッピング
- ページ名 URL 名前付きルート
- Contactページ
- 第3章の演習のとおり,Contactページについて追加
- RailsのルートURL
- 名前付きルートをサンプルアプリケーションの静的ページで使うために、ルーティング用のファイル (config/routes.rb) を編集
- ルートURLのようなルーティングを定義することの効果
- ブラウザからアクセスしやすくする
- 生のURLではなく、名前付きルートを使ってURLを参照することができる
- root_path: ルートURL以下の文字列
- root_url: 完全なURLの文字列を返します
- Railsチュートリアルでは一般的な規約に従い、基本的にはpath書式を使い、リダイレクトの場合のみurl書式を使う
- HTTPの標準としては、リダイレクトのときに完全なURLが要求されるため
- ほとんどのブラウザでは、どちらの方法でも動作します
- get '/help', to: 'static_pages#help'
- 演習
- 名前付きルートは、as:オプションを使って変更することができます
- get '/help', to: 'static_pages#help', as: 'helf'
- 名前付きルートは、as:オプションを使って変更することができます
- 名前付きルート
- link_toメソッドの2番目の引数で、適切な名前付きルートを使ってみましょう
- リンクのテスト
- 「統合テスト (Integration Test)」を使って一連の作業を自動化
- rails generate integration_test site_layout
- Railsは渡されたファイル名の末尾に _test という文字列を追加する
- assert_selectメソッドの高度なオプション
- 特定のリンクが存在するかどうかを、aタグとhref属性をオプションで指定して調べています
- assert_select "a[href=?]", about_path
- assert_select "a[href=?]", root_path, count: 2
- リンクの個数も調べる
- assert_selectは柔軟でパワフルな機能で、ここでは紹介し切れないほど他にも多くのオプションがあります
- 経験的には、このメソッドで複雑なテストはしない方が賢明
- 今回のようなレイアウト内で頻繁に変更されるHTML要素 (リンクなど) をテストするぐらいに抑えておくとよい
- rails test:integration
- 演習
- test環境でもApplicationヘルパーを使えるようにする
- ユーザー登録: 最初のステップ
- Usersコントローラ
- ユーザー登録用URL
- ユーザー登録URL用にget '/signup'のルートを追加(1つ上の演習と同じ)
- get '/signup', to: 'users#new'
- Usersコントローラのテストで名前付きルートを使うようにする
- ボタンにユーザー登録ページへのリンクを追加する
- 最初のユーザー登録ページ (スタブ)
- 演習
- get signup_path
- assert_select "title", full_title("Sign up")
- ユーザー登録URL用にget '/signup'のルートを追加(1つ上の演習と同じ)
- 最後に
- この章では、アプリケーションのレイアウトを形にし、ルーティングを洗練させました
- 本章のまとめ
- HTML5を使ってheaderやfooter、logoやbodyといったコンテンツのレイアウトを定義
- Railsのパーシャルは効率化のために使われ、別ファイルにマークアップを切り出すことができます
- CSSは、CSSクラスとidを使ってレイアウトやデザインを調整します
- Bootstrapフレームワークを使うと、いい感じのデザインを素早く実装できる
- SassとAsset Pipelineは、(開発効率のために切り分けられた) CSSの冗長な部分を圧縮し、本番環境に最適化した結果を出力する
- Railsのルーティングでは自由にルールを定義することができ、また、その際に名前付きルートも使えるようになる
- 統合テストは、ブラウザによるページ間の遷移を効率的にシミュレートする
Ch06 ユーザーのモデルを作成する
- 一番重要なステップであるユーザー用のデータモデルの作成と、データを保存する手段の確保について
- 第6章から第12章を通して、Railsのログインと認証システムをひととおり開発
- コラム 6.1. 自分で認証システムを作ってみる
- Userモデル
- ユーザー登録でまず初めにやることは、それらの情報を保存するためのデータ構造を作成すること
- Railsでは、データモデルとして扱うデフォルトのデータ構造のことをモデル (Model)
- Railsでは、データを永続化するデフォルトの解決策として、データベースを使ってデータを長期間保存
- データベースとやりとりをするデフォルトのRailsライブラリはActive Record
- Active Recordは、データオブジェクトの作成/保存/検索のためのメソッドを持っています
- これらのメソッドを使うのに、リレーショナルデータベースで使うSQL (Structured Query Language)2 を意識する必要はありません
- Active Recordは、データオブジェクトの作成/保存/検索のためのメソッドを持っています
- Railsにはマイグレーション (Migration) という機能
- Railsは、データベースの細部をほぼ完全に隠蔽し、切り離してくれます
- 実際、本書ではSQLiteを開発 (development) 環境で使い、また、PostgreSQLを (Herokuでの) 本番環境 (production) で使います (1.5) が、本番環境のデータの保存方法の詳細について考える必要はほとんどありません
- データベースの移行
- Railsでユーザーをモデリングするときは、属性を明示的に識別する必要がありません
- nameやemailといったカラム名を今のうちに考えておくことで、後ほどUserオブジェクトの各属性をActiveRecordに伝えるときに楽になります
- rails generate model User name:string email:string
- データベースで使いたい2つの属性をRailsに伝えます。このときに、これらの属性の型情報も一緒に渡します
- generateコマンドの結果のひとつとして、マイグレーションと呼ばれる新しいファイルが生成
- マイグレーションは、データベースの構造をインクリメンタルに変更する手段を提供
- マイグレーション自体は、データベースに与える変更を定義したchangeメソッドの集まり
- リスト 6.2の場合、changeメソッドはcreate_tableというRailsのメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成
- create_tableメソッドはブロック変数を1つ持つブロック (4.3.2) を受け取ります
- そのブロックの中でcreate_tableメソッドはtオブジェクトを使って、nameとemailカラムをデータベースに作ります
- モデル名は単数形 (User) ですが、テーブル名は複数形 (users) です
- モデルはひとりのユーザーを表すのに対し、データベースのテーブルは複数のユーザーから構成
- t.timestampsは特別なコマンドで、created_atとupdated_atという2つの「マジックカラム (Magic Columns)」を作成
- rails db:migrate
- 演習
- modelファイル
- class User < ApplicationRecord
- ユーザーオブジェクトを作成する
- rails console --sandbox
- 初期化ハッシュ (hash) を引数に取る
- 同様の方法でオブジェクトを初期化するActive Recordの設計
- Active Recordを理解する上で、「有効性 (Validity)」という概念も重要
- user.valid?
- ただオブジェクトが有効かどうかを確認しただけ
- データベースにUserオブジェクトを保存するためには、userオブジェクトからsaveメソッドを呼び出す必要
- モデルの生成と保存を2つのステップに分けておくと何かと便利
- しかし、Active RecordではUser.createでモデルの生成と保存を同時におこなう方法も提供されています
- User.create(name: "A Nother", email: "another@example.org")
- User.createは、trueかfalseを返す代わりに、ユーザーオブジェクト自身を返すことに注目
- destroyはcreateの逆
- 削除されたオブジェクトは次のようにまだメモリ上に残っています
- しかし、Active RecordではUser.createでモデルの生成と保存を同時におこなう方法も提供されています
- モデルの生成と保存を2つのステップに分けておくと何かと便利
- user.valid?
- ユーザーオブジェクトを検索する
- User.find(1)
- recordなし → findメソッドは例外 (exception) を発生
- 存在しないActive Recordのidによって、findでActiveRecord::RecordNotFoundという例外が発生
- User.find_by(email: "mhartl@example.com")
- find関連メソッドは、ユーザーをサイトにログインさせる方法を学ぶときに役に立ちます
- User.first
- User.all
- 演習
- User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認
- User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認
- ダックタイピング (duck typing)
- ユーザーオブジェクトを更新する
- user.email = "mhartl@example.net"
- 変更をデータベースに保存するために最後にsaveを実行
- 保存を行わずにreloadを実行すると、データベースの情報を元にオブジェクトを再読み込みするので、変更が取り消されます
- user.update_attributes(name: "The Dude", email: "dude@abides.org")
- 特定の属性のみを更新したい場合は、次のようにupdate_attribute
- user.update_attribute(:name, "El Duderino")
- 検証を回避
- 演習
- created_atも直接更新
- 更新するときは「1.year.ago」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます
- created_atも直接更新
- ユーザーを検証する
- Active Record では検証 (Validation) という機能を通して、こういった制約を課すことができる
- 存在性 (presence)の検証、長さ (length)の検証、フォーマット (format)の検証、一意性 (uniqueness)の検証
- 最終検証として確認 (confirmation)
- 有効性を検証する
- 具体的なテスト方法についてですが、まず有効なモデルのオブジェクトを作成し、その属性のうちの1つを有効でない属性に意図的に変更します。そして、バリデーションで失敗するかどうかをテストする、といった方針で進めてい
- test/models/user_test.rb
- setup
- rails test:models
- 存在性を検証する
- @user.name = " "
- assert_not @user.valid?
- name属性の存在を検査する方法は、リスト 6.9に示したとおり、validates メソッドにpresence: trueという引数を与えて使うこと
- validates :name, presence: true
- validatesは単なるメソッド
- presence: trueという引数は、要素が1つのオプションハッシュです。4.3.4のようにメソッドの最後の引数としてハッシュを渡す場合、波カッコを付けなくても問題ありません
- validatesは単なるメソッド
- user.errors.full_messages
- 演習
- u.errors.messagesを実行すると、ハッシュ形式でエラーが取得
- u.errors.messages[:email]
- 長さを検証する
- @user.email = "a" * 244 + "@example.com"
- :maximumパラメータと共に用いられる:lengthは、長さの上限を強制
- validates :name, presence: true, length: { maximum: 50 }
- 演習
- u = User.new(name: "a"51, email: "a"244 + "@example.com")
- :email=>["is too long (maximum is 255 characters)"]
- フォーマットを検証する
- valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn]
- assert @user.valid?, "#{valid_address.inspect} should be valid"
- assertメソッドの第2引数にエラーメッセージを追加していることに注目してください。これによって、どのメールアドレスでテストが失敗したのかを特定できるようになります
- メールアドレスのフォーマットを検証するためには、次のようにformatというオプションを使います
- 演習
- 一意性を検証する
- メールアドレスの一意性を強制するために (ユーザー名として使うために)、validatesメソッドの:uniqueオプションを使います
- 重大な警告
- 一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります
- まずは重複したメールアドレスからテスト
- duplicate_user = @user.dup
- @user.save
- assert_not duplicate_user.valid?
- 通常、メールアドレスでは大文字小文字が区別されません
- duplicate_user.email = @user.email.upcase
- uniqueness: { case_sensitive: false }
- Active Recordはデータベースのレベルでは一意性を保証していないという問題
- データベース上のemailのカラムにインデックス (index) を追加し、そのインデックスが一意であるようにすれば解決
- コラム 6.2. データベースのインデックス
- データベースにカラムを作成するとき、そのカラムでレコードを検索する (find) 必要があるかどうかを考えることは重要
- すべてのユーザーを最初から順に一人ずつ探していく
- データベースの世界では全表スキャン (Full-table Scan) として知られており、数千のユーザーがいる実際のサイトでは極めて不都合
- emailカラムにインデックスを追加することで、この問題を解決
- すべてのユーザーを最初から順に一人ずつ探していく
- データベースにカラムを作成するとき、そのカラムでレコードを検索する (find) 必要があるかどうかを考えることは重要
- 既に存在するモデルに構造を追加するので、次のようにmigrationジェネレーターを使ってマイグレーションを直接作成する必要があります
- テストDB用のサンプルデータが含まれているfixtures内で一意性の制限が保たれていないため、テストは red になります
- → 今は削除
- いくつかのデータベースのアダプタが、常に大文字小文字を区別するインデックス を使っているとは限らない問題への対処
- 今回は「データベースに保存される直前にすべての文字列を小文字に変換する」という対策
- Active Recordのコールバック (callback) メソッドを利用
- before_save { self.email = email.downcase }
- 演習
- assert_equal mixed_case_email.downcase, @user.reload.email
- email.downcase!
- メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります
- Active Record では検証 (Validation) という機能を通して、こういった制約を課すことができる
- セキュアなパスワードを追加する
- セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それを (そのままではなく) ハッシュ化したものをデータベースに保存
- ハッシュ化: ハッシュ関数を使って、入力されたデータを元に戻せない (不可逆な) データにする処理
- ユーザーの認証は、パスワードの送信、ハッシュ化、データベース内のハッシュ化された値との比較、という手順
- ハッシュ化されたパスワード
- セキュアなパスワードの実装は、has_secure_passwordというRailsのメソッドを呼び出すだけでほとんど終わってしまいます
- has_secure_password
- セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる
- 2つのペアの仮想的な属性 (passwordとpassword_confirmation) が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される
- authenticateメソッドが使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalseを返すメソッド)
- 魔術的なhas_secure_password機能を使えるようにするには、1つだけ条件
- モデル内にpassword_digestという属性が含まれていること
- digestという言葉は、暗号化用ハッシュ関数という用語が語源
- モデル内にpassword_digestという属性が含まれていること
- 専門用語としての「暗号」というのは、設計上元に戻すことができることを指します
- 「パスワードのハッシュ化」では元に戻せない (不可逆) という点が重要
- 「計算量的に元のパスワードを復元するのは困難である」という点を強調するために、暗号化ではなくハッシュ化という用語を使っています
- まずはpassword_digestカラム用の適切なマイグレーションを生成
- has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要
- bcrypt gemをGemfileに追加
- gem 'bcrypt', '3.1.12'
- ユーザーがセキュアなパスワードを持っている
- has_secure_passwordには、仮想的なpassword属性とpassword_confirmation属性に対してバリデーションをする機能も(強制的に)追加されている
- password: "foobar", password_confirmation: "foobar"
- 演習
- {:password=>["can't be blank"]}
- has_secure_passwordには、仮想的なpassword属性とpassword_confirmation属性に対してバリデーションをする機能も(強制的に)追加されている
- パスワードの最小文字数
- @user.password = @user.password_confirmation = "a" * 5
- 多重代入 (Multiple Assignment)
- validates :password, presence: true, length: { minimum: 6 }
- @user.password = @user.password_confirmation = "a" * 5
- ユーザーの作成と認証
- authenticateメソッド
- 間違ったパスワードを与えた結果、user.authenticateがfalseを返した
- 正しいパスワードを与えてみましょう。今度はauthenticateがそのユーザーオブジェクトを返すようになります
- authenticateがUserオブジェクトを返すことは重要ではなく、返ってきた値の論理値がtrueであることが重要
- !!でそのオブジェクトが対応する論理値オブジェクトに変換できることを思い出してください。この性質を利用すると、user.authenticateがいい感じに仕事をしてくれるようになります
- 演習
- user.update_attribute(:name, "test")
- authenticateメソッド
- セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それを (そのままではなく) ハッシュ化したものをデータベースに保存
- 最後に
- ゼロからUserモデルを作成し、そこにname属性やemail属性、パスワード属性を加えました
- それぞれの値を制限する多くの重要なバリデーションも追加
- 渡されたパスワードをセキュアに認証できる機能も実装
- たった12行でここまでの機能が実装
- Railsの注目に値する特長
- 本番環境でUserモデルを使うためには、heroku runコマンドを使ってHeroku上でもマイグレーションを走らせる必要があります
- heroku run rails db:migrate
- 本章のまとめ
- マイグレーションを使うことで、アプリケーションのデータモデルを修正することができる
- Active Recordを使うと、データモデルを作成したり操作したりするための多数のメソッドが使えるようになる
- Active Recordのバリデーションを使うと、モデルに対して制限を追加することができる
- よくあるバリデーションには、存在性・長さ・フォーマットなどがある
- 正規表現は謎めいて見えるが非常に強力である
- データベースにインデックスを追加することで検索効率が向上する。また、データベースレベルでの一意性を保証するためにも使われる
- has_secure_passwordメソッドを使うことで、モデルに対してセキュアなパスワードを追加することができる
Ch07 ユーザー登録
- ユーザーを表示する
- ユーザーの名前とプロフィール写真を表示するためのページを作成
- デバッグとRails環境
- Webサイトのレイアウトにデバッグ情報を追加
- コラム 7.1. Railsの3つの環境
- Railsにはテスト環境 (test)、開発環境 (development)、そして本番環境 (production) の3つの環境がデフォルトで装備されています。
- Rails consoleのデフォルトの環境はdevelopment
- RailsにはRailsというオブジェクトがあり、それには環境の論理値 (boolean) を取るenvという属性があります
- Rails.env.test?はテスト環境ではtrueを返し、それ以外の環境ではfalse
- rails console test
- rails server --environment production
- rails db:migrate RAILS_ENV=production
- console、server、migrateの3つのコマンドでは、デフォルト以外の環境を指定する方法がそれぞれ異なっており、混乱を招く可能性があります
- heroku run rails consoleというコマンドを打つことで、本番環境を確認
- Herokuは本番サイト用のプラットフォームなので、実行されるアプリケーションはすべて本番環境
- デバッグ出力をきれいに整形
- app/assets/stylesheets/custom.scss
- Sassのミックスイン機能 (ここではbox_sizing)
- CSSルールのグループをパッケージ化して複数の要素に適用
- @include box_sizing;
- デバッグ出力には、描画されるページの状態を把握するのに役立つ情報が含まれます
- paramsに含まれている内容で、YAML5 という形式で書かれています
- YAMLは基本的にハッシュであり、コントローラとページのアクションを一意に指定
- Usersリソース
- Railsアプリケーションで好まれているRESTアーキテクチャ (コラム 2.2) の習慣に従う
- resources :users
- この1行を追加すると、ユーザーのURLを生成するための多数の名前付きルート (5.3.3) と共に、RESTfulなUsersリソースで必要となるすべてのアクションが利用できるようになる → 表7.1
- 表 7.1: リスト 7.3のUsersリソースが提供するRESTfulなルート
- app/views/users/show.html.erb
- <%= @user.name %>, <%= @user.email %>
- ユーザー表示ビューが正常に動作するためには、Usersコントローラ内のshowアクションに対応する@user変数を定義する必要
- debuggerメソッド
- Gravatar画像とサイドバー
- Gravatar (Globally Recognized AVATAR) をプロフィールに導入
- app/views/users/show.html.erb
- <%= gravatar_for @user %>
- GravatarのURLはユーザーのメールアドレスをMD5という仕組みでハッシュ化
- app/helpers/users_helper.rb
- ユーザーのサイドバーの最初のバージョンを作りましょう
- asideタグを使って実装
- SCSSを使ってサイドバーなどのユーザー表示ページにスタイルを与える
- HTML要素とCSSクラスを配置したことにより、プロフィールページ (とサイドバーとGravatar) にSCSSでリスト 7.11のようにスタイルを与えることができるようになりました
- 演習
- ユーザー登録フォーム
- form_forを使用する
- ユーザー登録ページで重要な点は、ユーザー登録に欠かせない情報を入力するためのform
- Railsでform_forヘルパーメソッドを使います。このメソッドはActive Recordのオブジェクトを取り込み、そのオブジェクトの属性を使ってフォームを構築
- Rails 5.1から推奨されているform_withメソッドを使って学びたい方は『第6版』
- ユーザー登録ページ /signup のルーティングは、Usersコントローラーのnewアクションに既に紐付けられている
- 次のステップは、 form_forの引数で必要となるUserオブジェクトを作成すること
- newアクションに@user変数を追加
- app/views/users/new.html.erb
- <%= form_for(@user) do |f| %>
- app/assets/stylesheets/custom.scss
- フォームHTML
- <%= form_for(@user) do |f| %>
- doキーワードは、 form_forが1つの変数を持つブロックを取ることを表します。この変数fは “form” のf
- fオブジェクトは、HTMLフォーム要素 (テキストフィールド、ラジオボタン、パスワードフィールドなど) に対応するメソッドが呼び出されると、@userの属性を設定するために特別に設計されたHTMLを返します
- ユーザーの作成で重要なのはinputごとにある特殊なname属性
- Railsはnameの値を使って、初期化したハッシュを (params変数経由で) 構成
- このハッシュは、入力された値に基づいてユーザーを作成するときに使われます
- Railsはnameの値を使って、初期化したハッシュを (params変数経由で) 構成
- 次に重要な要素は、formタグ自身
- <%= form_for(@user) do |f| %>
- form_forを使用する
- ユーザー登録失敗
- 正しいフォーム
- createアクションでフォーム送信を受け取り、User.newを使って新しいユーザーオブジェクトを作成し、ユーザーを保存 (または保存に失敗) し、再度の送信用のユーザー登録ページを表示するという方法で機能を実装
- app/controllers/users_controller.rb
- if-else分岐構造を思い出してください。この文を使って、保存が成功したかどうかに応じて@user.saveの値がtrueまたはfalse (6.1.3) になるときに、それぞれ成功時の処理と失敗時の処理を場合分け
- Strong Parameters
- マスアサインメント
- 値のハッシュを使ってRubyの変数を初期化
- @user = User.new(params[:user])
- paramsハッシュ全体を初期化するという行為はセキュリティ上、極めて危険
- Strong Parametersを使うことで、必須のパラメータと許可されたパラメータを指定することができます
- paramsハッシュをまるごと渡すとエラーが発生するので、Railsはデフォルトでマスアサインメントの脆弱性から守られるようになりました
- params.require(:user).permit(:name, :email, :password, :password_confirmation)
- 戻り値は、許可された属性のみが含まれたparamsのハッシュ
- これらのパラメータを使いやすくするために、user_paramsという外部メソッドを使うのが慣習
- app/controllers/users_controller.rb
- user_paramsメソッドはUsersコントローラの内部でのみ実行され、Web経由で外部ユーザーにさらされる必要はないため、リスト 7.19に示すようにRubyのprivateキーワードを使って外部から使えないようにします
- app/controllers/users_controller.rb
- マスアサインメント
- エラーメッセージ
- ユーザーのnewページでエラーメッセージのパーシャル (partial) を出力
- form-controlというCSSクラスも一緒に追加することで、Bootstrapがうまく取り扱ってくれる
- app/views/shared/_error_messages.html.erb
- 'shared/error_messages'というパーシャルをrender (描画) している
- Rails全般の慣習として、複数のビューで使われるパーシャルは専用のディレクトリ「shared」によく置かれます
- app/views/shared/_error_messages.html.erb
- pluralizeという英語専用のテキストヘルパー
- 最初の引数に整数が与えられると、それに基づいて2番目の引数の英単語を複数形に変更したものを返します
- このメソッドの背後には強力なインフレクター (活用形生成) があり、不規則活用を含むさまざまな単語を複数形にすることができます
- エラーメッセージにスタイルを与えるためのCSS id error_explanationも含まれている
- pluralizeという英語専用のテキストヘルパー
- Railsは、無効な内容の送信によって元のページに戻されると、CSSクラスfield_with_errorsを持ったdivタグでエラー箇所を自動的に囲んでくれます
- このラベルを使うことで、リスト 7.22のようにエラーメッセージをSCSSで整形
- Sassの@extend関数を使ってBootstrapのhas-errorというCSSクラスを適用
- ユーザーのnewページでエラーメッセージのパーシャル (partial) を出力
- 失敗時のテスト
- まずは、新規ユーザー登録用の統合テストを生成するところから始めていきます
- 演習
- assert_select 'div#error_explanation'
- assert_select 'div.field_with_errors'
- post '/signup', to: 'users#create'
- <%= form_for(@user, url: signup_path) do |f| %>
- assert_select 'form[action="/signup"]'
- 正しいフォーム
- ユーザー登録成功
- 登録フォームの完成
- ユーザー登録に成功した場合はページを描画せずに別のページにリダイレクト (Redirect) する
- 新しく作成されたユーザーのプロフィールページにリダイレクト
- redirect_to @user
- redirect_to user_url(@user)と同じ
- ユーザー登録に成功した場合はページを描画せずに別のページにリダイレクト (Redirect) する
- flash
- Webアプリケーションに常識的に備わっている機能を追加してみましょう。
- 登録完了後に表示されるページにメッセージを表示し (この場合は新規ユーザーへのウェルカムメッセージ)、2度目以降にはそのページにメッセージを表示しないようにするというもの
- Railsでこういった情報を表示するためには、flashという特殊な変数
- flash[:success] = "Welcome to the Sample App!"
- Welcome to the Sample App!になる
- Bootstrap CSSは、このようなflashのクラス用に4つのスタイルを持っています (success、info、warning、danger)
- app/views/layouts/application.html.erb
- 演習
- "#{:success}" → "success"
- Webアプリケーションに常識的に備わっている機能を追加してみましょう。
- 実際のユーザー登録
- 成功時のテスト
- 登録フォームの完成
- プロのデプロイ
- 本番環境でのSSL
- Webサイト全体で適用できるため、第8章で実装するログイン機構をセキュアにしたり、9.1.で説明するセッションハイジャック (Session Hijacking) の脆弱性に対しても多くの利点を生み出します
- SSLを有効化するのも簡単です。production.rbという本番環境の設定ファイルの1行を修正するだけで済みます
- Herokuではデフォルトの設定でもSSLを使用できるのですが、SSLの使用をブラウザに強制するわけではありません
- Railsではありがたいことに、本番環境用の設定ファイルであるproduction.rbのコードをたった1行変更するだけでSSLを強制し、httpsによる安全な通信を確立
- config/environments/production.rb
- config.force_ssl = true
- Heroku上でサンプルアプリケーションを動かし、HerokuのSSL証明書に便乗
- Herokuのサブドメインでのみ有効
- www.example.comなどの独自ドメインでSSLを使いたい場合は、HerokuのSSLに関するドキュメント (英語) を参照
- 本番環境用のWebサーバー
- 本番環境へのデプロイ
- 本番環境でのSSL
- 最後に
- 本章のまとめ
- debugメソッドを使うことで、役立つデバッグ情報を表示できる
- Sassのmixin機能を使うと、CSSのルールをまとめたり他の場所で再利用できるようになる
- Railsには標準で3つ環境が備わっており、それぞれ開発環境 (development)、テスト環境 (test)、本番環境 (production)と呼ぶ
- 標準的なRESTfulなURLを通して、ユーザー情報をリソースとして扱えるようになった
- Gravatarを使うと、ユーザーのプロフィール画像を簡単に表示できるようになる
- form_forヘルパーは、Active Recordのオブジェクトに対応したフォームを生成する
- ユーザー登録に失敗した場合はnewビューを再描画するようにした。その際、Active Recordが自動的に検知したエラーメッセージを表示できるようにした
- flash変数を使うと、一時的なメッセージを表示できるようになる
- ユーザー登録に成功すると、データベース上にユーザーが追加、プロフィールページにリダイレクト、ウェルカムメッセージの表示といった順で処理が進む
- 統合テストを使うことで送信フォームの振る舞いを検証したり、バグの発生を検知したりできる
- セキュアな通信と高いパフォーマンスを確保するために、本番環境ではSSLとPumaを導入した
- 本章のまとめ
Ch08 基本的なログイン機構
- ログインの基本的な仕組みとは、ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄するといった仕組み (認証システム (Authentification System))
- このような制限や制御の仕組みを認可モデル (Authorization Model) と呼び、例えば本章で実装するログイン済みかどうかでヘッダー部分を切り替える、といった仕組みもこれにあたります
- セッション
- HTTPはステートレス (Stateless) なプロトコル
- ユーザーログインの必要なWebアプリケーションでは、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に別途設定
- Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法
- cookiesとは、ユーザーのブラウザに保存される小さなテキストデータ
- アプリケーションはcookies内のデータを使って、例えばログイン中のユーザーが所有する情報をデータベースから取り出すことができます
- 本節および8.2では、sessionというRailsのメソッドを使って一時セッションを作成
- セッションをRESTfulなリソースとしてモデリングできると、他のRESTfulリソースと統一的に理解できて便利
- ログインページではnewで新しいセッションを出力し、そのページでログインするとcreateでセッションを実際に作成して保存し、ログアウトするとdestroyでセッションを破棄する
- Usersリソースと異なるのは、UsersリソースではバックエンドでUserモデルを介してデータベース上の永続的データにアクセスするのに対し、Sessionリソースでは代わりにcookiesを保存場所として使う点
- ログインの仕組みの大半は、cookiesを使った認証メカニズムによって構築
- 本節と次の節では、セッション機能を作成する準備として、Sessionコントローラ、ログイン用のフォーム、両者に関連するコントローラのアクションを作成
- Sessionsコントローラ
- ログインフォーム
- 7.3.3ではエラーメッセージの表示に専用のパーシャルを使いましたが、そのパーシャルではActive Recordによって自動生成されるメッセージを使っていた
- 今回は、フラッシュメッセージでエラーを表示
- app/views/sessions/new.html.erb
- セッションにはSessionモデルというものがなく、そのため@userのようなインスタンス変数に相当するものもない
- 新しいセッションフォームを作成するときには、form_forヘルパーに追加の情報を独自に渡さなければなりません
- form_for(:session, url: login_path)
- ユーザーの検索と認証
- ログインでセッションを作成する場合に最初に行うのは、入力が無効な場合の処理
- 最初に最小限のcreateアクションをSessionsコントローラで定義し、空のnewアクションとdestroyアクションも作成
- createアクションの中では、ユーザーの認証に必要なあらゆる情報をparamsハッシュから簡単に取り出せる
- app/controllers/sessions_controller.rb
- user = User.find_by(email: params[:session][:email].downcase)
- if user && user.authenticate(params[:session][:password])
- フラッシュメッセージを表示する
- フラッシュのテスト
- ログイン
- ログイン中の状態での有効な値の送信をフォームで正しく扱えるようにします
- cookiesを使った一時セッションでユーザーをログインできるようにします
- app/controllers/application_controller.rb
- include SessionsHelper
- log_inメソッド
- session[:user_id] = user.id
- ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成
- cookiesメソッド (9.1) とは対照的に、sessionメソッドで作成された一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了
- app/helpers/sessions_helper.rb
- def log_in(user)
- session[:user_id] = user.id
- def log_in(user)
- sessionメソッドで作成した一時cookiesは自動的に暗号化され、リスト 8.14のコードは保護されます
- 攻撃者がたとえこの情報をcookiesから盗み出すことができたとしても、それを使って本物のユーザーとしてログインすることはできない
- ただし今述べたことは、sessionメソッドで作成した「一時セッション」にしか該当しません
- cookiesメソッドで作成した「永続的セッション」ではそこまで断言はできません
- 永続的なcookiesには、セッションハイジャックという攻撃を受ける可能性が常につきまといます
- log_inというヘルパーメソッドを定義できたので、やっと、ユーザーログインを行ってセッションのcreateアクションを完了し、ユーザーのプロフィールページにリダイレクトする準備ができました
- app/controllers/sessions_controller.rb
- log_in user
- redirect_to user
- app/controllers/sessions_controller.rb
- session[:user_id] = user.id
- 現在のユーザー
- if session[:user_id]
- セッションにユーザーIDが存在しない場合、このコードは単に終了して自動的にnilを返します
- @current_user ||= User.find_by(id: session[:user_id])
- app/helpers/sessions_helper.rb
- def current_user
- if session[:user_id]
- @current_user ||= User.find_by(id: session[:user_id])
- if session[:user_id]
- def current_user
- if session[:user_id]
- レイアウトリンクを変更する
- app/helpers/sessions_helper.rb
- def logged_in?
- !current_user.nil?
- def logged_in?
- app/views/layouts/_header.html.erb
- <% if logged_in? %>
- <%= link_to "Profile", current_user %>
- current_userを使う方が、Railsによってuser_path(current_user)に変換され、プロフィールへのリンクが自動的に生成できるので便利
- <%= link_to "Log out", logout_path, method: :delete %>
- <%= link_to "Log in", login_path %>
- app/helpers/sessions_helper.rb
- application.jsにBootstrapのJavaScriptライブラリを追加する
- app/assets/javascripts/application.js
- //= require jquery
- //= require bootstrap
- app/assets/javascripts/application.js
- テスト用データをfixture (フィクスチャ) で作成
- Railsのsecure_passwordのソースコードを調べてみると、次の部分でパスワードが生成
- BCrypt::Password.create(string, cost: cost)
- costはコストパラメータ
- コストパラメータの値を高くすれば、ハッシュからオリジナルのパスワードを計算で推測することが困難
- 本番環境ではセキュリティ上重要
- cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
- BCrypt::Password.create(string, cost: cost)
- fixture向けのdigestメソッドを追加
- app/models/user.rb
- ユーザーログインのテストで使うfixture
- test/fixtures/users.yml
- password_digest: <%= User.digest('password') %>
- テスト用のfixtureでは全員同じパスワード「password」を使う
- test/fixtures/users.yml
- test/integration/users_login_test.rb
- def setup
- @user = users(:michael)
- test "login with valid information" do
- assert_redirected_to @user
- follow_redirect!
- assert_select "a[href=?]", login_path, count: 0
- def setup
- ユーザー登録中にログインするには、Usersコントローラのcreateアクションにlog_inを追加するだけで済みます
- app/controllers/users_controller.rb
- if @user.save
- log_in @user
- if @user.save
- app/controllers/users_controller.rb
- テスト中のログインステータスを論理値で返すメソッド
- test/test_helper.rb
- def is_logged_in?
- test/test_helper.rb
- test/integration/users_signup_test.rb
- assert is_logged_in?
- ログインの場合 (リスト 8.15とリスト 8.25) と異なり、ログアウト処理は1か所で行える
- app/helpers/sessions_helper.rb
- def log_out
- session.delete(:user_id)
- @current_user = nil
- def log_out
- app/controllers/sessions_controller.rb
- log_out
- redirect_to root_url
- test/integration/users_login_test.rb
- test "login with valid information followed by logout" do
- 本章では、サンプルアプリケーションの基本的なログイン機構 (認証システム) を実装
- 本章のまとめ
- Railsのsessionメソッドを使うと、あるページから別のページに移動するときの状態を保持できる。一時的な状態の保存にはcookiesも使える
- ログインフォームでは、ユーザーがログインするための新しいセッションが作成できる
- flash.nowメソッドを使うと、描画済みのページにもフラッシュメッセージを表示できる
- テスト駆動開発は、回帰バグを防ぐときに便利
- sessionメソッドを使うと、ユーザーIDなどをブラウザに一時的に保存できる
- ログインの状態に応じて、ページ内で表示するリンクを切り替えることができる
- 統合テストでは、ルーティング、データベースの更新、レイアウトの変更が正しく行われているかを確認できる
Ch09 発展的なログイン機構
- ユーザーのログイン情報を記憶しておき、ブラウザを再起動した後でもすぐにログインできる機能 (remember me) を備えていることも一般的
- 永続クッキー (permanent cookies) を使ってこの機能を実現
- Remember me 機能
- 記憶トークンと暗号化
- セッションの永続化の第一歩として記憶トークン (remember token) を生成
- cookiesメソッドによる永続的cookiesの作成や、安全性の高い記憶ダイジェスト (remember digest) によるトークン認証にこの記憶トークンを活用
- パスワードとトークンとの一般的な違いは、パスワードはユーザーが作成・管理する情報であるのに対し、トークンはコンピューターが作成・管理する情報である点
- cookiesを盗み出す有名な方法
- rails generate migration add_remember_digest_to_users remember_digest:string
- 記憶トークンとして何を使うかを決める
- ユーザーを記憶するには、記憶トークンを作成して、そのトークンをダイジェストに変換したものをデータベースに保存
- トークン生成用メソッドを追加する
- app/models/user.rb
- def User.new_token
- SecureRandom.urlsafe_base64
- def User.new_token
- app/models/user.rb
- user.rememberメソッドを作成
- 演習
- selfは、通常の文脈ではUser「モデル」、つまりユーザーオブジェクトのインスタンスを指しますが、リスト 9.4やリスト 9.5の文脈では、selfはUser「クラス」を指す
- def self.digest(string)
- class << self
- selfは、通常の文脈ではUser「モデル」、つまりユーザーオブジェクトのインスタンスを指しますが、リスト 9.4やリスト 9.5の文脈では、selfはUser「クラス」を指す
- ログイン状態の保持
- user.rememberメソッドが動作するようになったので、ユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存して、永続セッションを作成する準備ができました
- 実際に行うにはcookiesメソッドを使います
- このメソッドは、sessionのときと同様にハッシュとして扱えます
- 個別のcookiesは、1つのvalue (値) と、オプションのexpires (有効期限) からできています
- cookies[:remember_token] = { value: remember_token, expires: 20.years.from_now.utc }
- ユーザーIDをcookiesに保存
- cookies.permanent.signed[:user_id] = user.id
- User.find_by(id: cookies.signed[:user_id])
- cookies.signed[:user_id]では自動的にユーザーIDのcookiesの暗号が解除され、元に戻ります
- 渡されたトークンがユーザーの記憶ダイジェストと一致することを確認
- BCrypt gemでは,比較に使っている==演算子が再定義されている
- BCrypt::Password.new(remember_digest).is_password?(remember_token)を使っている
- authenticated?をUserモデルに追加する
- app/models/user.rb
- BCrypt::Password.new(remember_digest).is_password?(remember_token)
- app/models/user.rb
- BCrypt gemでは,比較に使っている==演算子が再定義されている
- ログインしてユーザーを保持する
- app/controllers/sessions_controller.rb
- remember user
- app/controllers/sessions_controller.rb
- ユーザーを記憶する
- app/helpers/sessions_helper.rb
- def remember(user)
- app/helpers/sessions_helper.rb
- 永続的セッションのcurrent_userを更新する
- def current_user
- ユーザーを忘れる
- forgetメソッドをUserモデルに追加
- update_attribute(:remember_digest, nil)
- 永続セッションからログアウト
- app/helpers/sessions_helper.rb
- forgetヘルパーメソッドではuser.forgetを呼んでからuser_idとremember_tokenのcookiesを削除
- forgetメソッドをUserモデルに追加
- 2つの目立たないバグ
- 記憶トークンと暗号化
- [Remember me] チェックボックス
- [remember me] チェックボックスをログインフォームに追加
- [remember me] チェックボックスのCSS
- [remember me] チェックボックスの送信結果を処理
- app/controllers/sessions_controller.rb
- params[:session][:remember_me] == '1' ? remember(user) : forget(user)
- コラム 9.2. 10種類の人々
- 「この世には10種類の人間がいる。2進法を理解できる奴と、2進法を理解できない奴だ」
- 三項演算子 (ternary operator)
- 論理値? ? 何かをする : 別のことをする
- app/controllers/sessions_controller.rb
- [Remember me] のテスト
- [Remember me] ボックスをテストする
- [Remember me] をテストする
- テストを忘れている疑いのあるコードブロック内にわざと例外発生を仕込む
- raise
- test/helpers/sessions_helper_test.rb
- assert_equal @user, current_user
- assert is_logged_in?
- @user.update_attribute(:remember_digest, User.digest(User.new_token))
- assert_nil current_user
- テストを忘れている疑いのあるコードブロック内にわざと例外発生を仕込む
- 最後に
Ch10 ユーザーの更新・表示・削除
- Usersリソース用のRESTアクション (表 7.1) のうち、これまで未実装だったedit、update、index、destroyアクションを加え、RESTアクションを完成させます
- 認可モデル (Authorization Model) について
- サンプルデータとページネーション (pagination) を導入
- ユーザーを更新する
- ユーザー情報を編集するパターンは、新規ユーザーの作成と極めて似通っています
- newアクションと同じようにして、ユーザーを編集するためのeditアクションを作成
- POSTリクエストに応答するcreateの代わりに、PATCHリクエストに応答するupdateアクションを作成
- 最大の違いは、ユーザー登録は誰でも実行できますが、ユーザー情報を更新できるのはそのユーザー自身に限られるということ
- beforeフィルター (before filter) を使ってこのアクセス制御を実現
- 編集フォーム
- ユーザーのeditアクション
- app/controllers/users_controller.rb
- @user = User.find(params[:id])
- app/controllers/users_controller.rb
- ユーザーのeditビュー
- Railsはどうやって新規ユーザー用のPOSTリクエストとユーザー編集用のPATCHリクエストを区別するのでしょうか
- Railsは、ユーザーが新規なのか、それともデータベースに存在する既存のユーザーであるかを、Active Recordのnew_record?論理値メソッドを使って区別できる
- form_for(@user)を使ってフォームを構成すると、@user.new_record?がtrueのときにはPOSTを、falseのときにはPATCHを使います
- レイアウトの “Settings” リンクを更新する
- app/views/layouts/_header.html.erb
- <\li><%= link_to "Settings", edit_user_path(current_user) %><\/li>
- app/views/layouts/_header.html.erb
- 演習
- target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点
- 具体的には、フィッシング (Phising) サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります
- 対処方法は、リンク用のaタグのrel (relationship) 属性に、"noopener"と設定するだけ
- newとeditフォーム用のパーシャル
- <% provide(:button_text, "Save changes") %>
- <%= render 'form' %>
- view以下のパスを書くが,同階層ならファイル名だけでよいようだ
- partialを示す_は不要
- target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点
- ユーザーのeditアクション
- 編集の失敗
- 編集失敗時のテスト
- TDDで編集を成功させる
- より快適にテストをするためには、アプリケーション用のコードを「実装する前に」統合テストを書いた方が便利
- そういったテストのことは「受け入れテスト (Acceptance Tests)」
- ある機能の実装が完了し、受け入れ可能な状態になったかどうかを決めるテスト
- ユーザー名やメールアドレスを編集するときに毎回パスワードを入力するのは不便なので、(パスワードを変更する必要が無いときは) パスワードを入力せずに更新できると便利
- @user.reloadを使って、データベースから最新のユーザー情報を読み込み直して、正しく更新されたかどうかを確認
- test/integration/users_edit_test.rb
- ユーザーのupdateアクション
- flash[:success] = "Profile updated"
- redirect_to @user
- パスワードが空のままでも更新できるようにする
- より快適にテストをするためには、アプリケーション用のコードを「実装する前に」統合テストを書いた方が便利
- 認可
- ウェブアプリケーションの文脈では、認証 (authentication) はサイトのユーザーを識別すること
- 認可 (authorization) はそのユーザーが実行可能な操作を管理すること
- この節では、ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御
- こういったセキュリティ上の制御機構をセキュリティモデルと呼びます
- ユーザーにログインを要求する
- 転送させる仕組みを実装したいときは、Usersコントローラの中でbeforeフィルターを使います
- beforeフィルターは、before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組み
- beforeフィルターにlogged_in_userを追加
- app/controllers/users_controller.rb
- before_action :logged_in_user, only: [:edit, :update]
- :onlyオプション (ハッシュ) を渡すことで、:editと:updateアクションだけにこのフィルタが適用されるように制限
- unless logged_in?
- flash[:danger] = "Please log in."
- redirect_to login_url
- before_action :logged_in_user, only: [:edit, :update]
- app/controllers/users_controller.rb
- テストユーザーでログインする
- test/integration/users_edit_test.rb
- log_in_as(@user)
- test/integration/users_edit_test.rb
- セキュリティモデルを確認するためにbeforeフィルターをコメントアウト
- editとupdateアクションの保護に対するテストする
- 転送させる仕組みを実装したいときは、Usersコントローラの中でbeforeフィルターを使います
- 正しいユーザーを要求する
- ユーザーが自分の情報だけを編集できるようにする必要
- セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます
- fixtureファイルに2人目のユーザーを追加
- test/fixtures/users.yml
- log_in_asメソッドを使って、editアクションとupdateアクションをテスト
- 間違ったユーザーが編集しようとしたときのテスト
- 既にログイン済みのユーザーを対象としているため、ログインページではなくルートURLにリダイレクト
- 間違ったユーザーが編集しようとしたときのテスト
- beforeフィルターを使って編集/更新ページを保護
- correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出す
- redirect_to(root_url) unless current_user?(@user)
- correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出す
- current_user?メソッド
- app/helpers/sessions_helper.rb
- def current_user?(user)
- user == current_user
- def current_user?(user)
- app/helpers/sessions_helper.rb
- フレンドリーフォワーディング
- 保護されたページにアクセスしようとすると、問答無用で自分のプロフィールページに移動させられてしまいます
- リダイレクト先は、ユーザーが開こうとしていたページにしてあげるのが親切
- 実際のテストはまず編集ページにアクセスし、ログインした後に、(デフォルトのプロフィールページではなく) 編集ページにリダイレクトされているかどうかをチェックする
- get edit_user_path(@user)
- log_in_as(@user)
- assert_redirected_to edit_user_url(@user)
- フレンドリーフォワーディングの実装
- app/helpers/sessions_helper.rb
- def redirect_back_or(default)
- redirect_to(session[:forwarding_url] || default)
- session.delete(:forwarding_url)
- これをやっておかないと、次回ログインしたときに保護されたページに転送されてしまい、ブラウザを閉じるまでこれが繰り返されてしまいます
- 転送用のURLを削除する動作はredirect文の後に置かれていても実行される
- 明示的にreturn文やメソッド内の最終行が呼び出されない限り、リダイレクトは発生しません
- def store_location
- session[:forwarding_url] = request.original_url if request.get?
- def redirect_back_or(default)
- app/controllers/users_controller.rb
- app/controllers/sessions_controller.rb
- requestオブジェクトも使っています (request.original_urlでリクエスト先が取得できます)
- POSTや PATCH、DELETEリクエストを期待しているURLに対して、(リダイレクトを通して) GETリクエストが送られてしまい、場合によってはエラーが発生します
- if request.get?という条件文を使ってこのケースに対応
- ログインユーザー用beforeフィルターにstore_locationを追加
- デフォルトのURLは、Sessionコントローラのcreateアクションに追加し、サインイン成功後にリダイレクト
- 演習
- test/integration/users_edit_test.rb
- assert_nil session[:forwarding_url]
- test/integration/users_edit_test.rb
- app/helpers/sessions_helper.rb
- すべてのユーザーを表示する
- ユーザーの一覧ページ
- indexアクションのリダイレクトをテスト
- test "should redirect index when not logged in" do
- beforeフィルターのlogged_in_userにindexアクションを追加して、このアクションを保護
- ユーザーのindexアクション
- @users = User.all
- ユーザーのindexビュー
- ユーザーを列挙してユーザーごとにliタグで囲むビューを作成
- eachメソッドを使って作成
- ユーザーを列挙してユーザーごとにliタグで囲むビューを作成
- gravatar_forヘルパーにオプション引数を追加する
- ユーザーのindexページ用のCSS
- ユーザー一覧ページへのリンクを更新
- app/views/layouts/_header.html.erb
- 演習
- test/integration/site_layout_test.rb
- userをセットアップして,セットアップしたユーザでログイン
- routeをgetして各linkをselectする
- test/integration/site_layout_test.rb
- indexアクションのリダイレクトをテスト
- サンプルのユーザー
- ページネーション
- Railsには豊富なページネーションメソッドがあります。今回はその中で最もシンプルかつ堅牢なwill_paginateメソッドを使ってみましょう
- Gemfileにwill_paginate gem とbootstrap-will_paginate gemを両方含め、Bootstrapのページネーションスタイルを使ってwill_paginateを構成する
- indexページでpaginationを使う
- <%= will_paginate %>
- paginateでは、キーが:pageで値がページ番号のハッシュを引数に取ります
- User.paginateは、:pageパラメーターに基いて、データベースからひとかたまりのデータ (デフォルトでは30) を取り出し
- pageがnilの場合、 paginateは単に最初のページを返します
- indexアクションでUsersをページネート
- @users = User.paginate(page: params[:page])
- :pageパラメーターにはparams[:page]が使われていますが、これはwill_paginateによって自動的に生成
- ユーザー一覧のテスト
- 今回のテストでは、ログイン、indexページにアクセス、最初のページにユーザーがいることを確認、ページネーションのリンクがあることを確認、といった順でテスト
- fixtureでは埋め込みRubyをサポート
- fixtureにさらに30人のユーザーを追加する
- <% 30.times do |n| %>
- user_<%= n %>:
- name: <%= "User #{n}" %>
- rails generate integration_test users_index
- paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認
- User.paginate(page: 1).each do |user|
- assert_select 'a[href=?]', user_path(user), text: user.name
- User.paginate(page: 1).each do |user|
- 演習
- assert_select 'div.pagination', count: 2
- パーシャルのリファクタリング
- ユーザーのliをrender呼び出しに置き換え
- <%= render user %>
- renderをパーシャル (ファイル名の文字列) に対してではなく、Userクラスのuser変数に対して実行
- Railsは自動的に_user.html.erbという名前のパーシャルを探しにいく
- <%= render user %>
- app/views/users/_user.html.erb
- renderを@users変数に対して直接実行
- ユーザーのliをrender呼び出しに置き換え
- ユーザーの一覧ページ
- ユーザーを削除する
- 管理ユーザー
- 論理値をとるadmin属性をUserモデルに追加
- rails generate migration add_admin_to_users admin:boolean
- default: falseという引数をadd_columnに追加
- サンプルデータ生成タスクに管理者を1人追加
- db/seeds.rb
- Strong Parameters、再び
- 演習
- admin属性の変更が禁止されていることをテスト
- test/controllers/users_controller_test.rb
- patch user_path(@other_user), params: { user: { password: @other_user.password, password_confirmation: @other_user.password,admin: true } }
- assert_not @other_user.reload.admin?
- 論理値をとるadmin属性をUserモデルに追加
- destroyアクション
- Usersリソースの最後の仕上げとして、destroyアクションへのリンクを追加
- まず、ユーザーindexページの各ユーザーに削除用のリンクを追加
- 続いて管理ユーザーへのアクセスを制限
- app/views/users/_user.html.erb
- <% if current_user.admin? && !current_user?(user) %>
- | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %>
- 必要なDELETEリクエストを発行するリンクの生成は、method: :deleteによって行われている
- <% if current_user.admin? && !current_user?(user) %>
- ブラウザはネイティブではDELETEリクエストを送信できないため、RailsではJavaScriptを使って偽造
- JavaScriptをサポートしないブラウザをサポートする必要がある場合は、フォームとPOSTリクエストを使ってDELETEリクエストを偽造することもできます。こちらはJavaScriptがなくても動作
- destroyアクション (表 7.1) を追加
- 該当するユーザーを見つけてActive Recordのdestroyメソッドを使って削除し、最後にユーザーindexに移動
- User.find(params[:id]).destroy
- flash[:success] = "User deleted"
- redirect_to users_url
- :destroyアクションもlogged_in_userフィルターに追加
- 該当するユーザーを見つけてActive Recordのdestroyメソッドを使って削除し、最後にユーザーindexに移動
- サイトを正しく防衛するには、destroyアクションにもアクセス制御を行う必要
- beforeフィルターを使ってdestroyアクションへのアクセスを制御
- before_action :admin_user, only: :destroy
- redirect_to(root_url) unless current_user.admin?
- beforeフィルターを使ってdestroyアクションへのアクセスを制御
- ユーザー削除のテスト
- fixture内の最初のユーザーを管理者にする
- Usersコントローラをテストするために、アクション単位でアクセス制御をテスト
- 2つのケースをチェック
- ログインしていないユーザーであれば、ログイン画面にリダイレクト
- ログイン済みではあっても管理者でなければ、ホーム画面にリダイレクト
- log_in_as(@other_user)
- assert_no_difference 'User.count' do
- delete user_path(@user)
- assert_redirected_to login_url
- 管理者ではないユーザーの振る舞いについて検証していますが、管理者ユーザーの振る舞いと一緒に確認できるとよさそう
- 2つのケースをチェック
- 削除リンクとユーザー削除に対する統合テスト
- test/integration/users_index_test.rb
- test "index as admin including pagination and delete links" do
- 各ユーザーの削除リンクをテストするときに、ユーザーが管理者であればスキップしている
- first_page_of_users = User.paginate(page: 1)
- first_page_of_users.each do |user|
- assert_select 'a[href=?]', user_path(user), text: user.name
- unless user == @admin
- assert_select 'a[href=?]', user_path(user), text: 'delete'
- unless user == @admin
- assert_select 'a[href=?]', user_path(user), text: user.name
- assert_difference 'User.count', -1 do
- delete user_path(@non_admin)
- test "index as non-admin" do
- assert_select 'a', text: 'delete', count: 0
- test "index as admin including pagination and delete links" do
- test/integration/users_index_test.rb
- 管理ユーザー
- 最後に
- 今は登録もログインもログアウトもできます
- プロフィールの表示も、設定の編集も、すべてのユーザーの一覧画面もあります
- 一部のユーザーは他のユーザーを削除することすらできるようになりました
- この時点で、サンプルアプリケーションはWebサイトとしての十分な基盤 (ユーザーを認証したり認可したり) が整った
- アプリケーションを本番展開したり、サンプルデータを本番データとして作成することもできます (本番データベースをリセットするにはpg:resetタスクを使います)
- 本章のまとめ
- ユーザーは、編集フォームからPATCHリクエストをupdateアクションに対して送信し、情報を更新する
- Strong Parametersを使うことで、安全にWeb上から更新させることができる
- beforeフィルターを使うと、特定のアクションが実行される直前にメソッドを呼び出すことができる
- beforeフィルターを使って、認可 (アクセス制御) を実現した
- 認可に対するテストでは、特定のHTTPリクエストを直接送信する低級なテストと、ブラウザの操作をシミュレーションする高級なテスト (統合テスト) の2つを利用した
- フレンドリーフォワーディングとは、ログイン成功時に元々行きたかったページに転送させる機能である
- ユーザー一覧ページでは、すべてのユーザーをページ毎に分割して表示する
- rails db:seedコマンドは、db/seeds.rbにあるサンプルデータをデータベースに流し込む
- render @usersを実行すると、自動的に_user.html.erbパーシャルを参照し、各ユーザーをコレクションとして表示する
- boolean型のadmin属性をUserモデルに追加すると、admin?という論理オブジェクトを返すメソッドが自動的に追加される
- 管理者が削除リンクをクリックすると、DELETEリクエストがdestroyアクションに向けて送信され、該当するユーザーが削除される
- fixtureファイル内で埋め込みRubyを使うと、多量のテストユーザーを作成することができる
Ch11 アカウントの有効化
- アカウント有効化やパスワード再設定の仕組みと、以前に実装したパスワードや記憶トークンの仕組みにはよく似た点が多い
- 多くのアイデアを使い回すことができます (具体的にはUser.digestやUser.new_token、改造版のuser.authenticated?メソッドなど)
- AccountActivationsリソース
- セッション機能 (8.1) を使って、アカウントの有効化という作業を「リソース」としてモデル化する
- AccountActivationsコントローラ
- AccountActivationのデータモデル
- rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
- add_column :users, :activated, :boolean, default: false
- Activationトークンのコールバック
- before_create :create_activation_digest
- メソッド参照と呼ばれるもので、こうするとRailsはcreate_activation_digestというメソッドを探し、ユーザーを作成する前に実行
- before_create :create_activation_digest
- Userモデルにアカウント有効化のコードを追加
- メールアドレスを小文字にするメソッドも (リスト 6.32)、メソッド参照に切り替え
- サンプルユーザーの生成とテスト
- サンプルデータとfixtureも更新し、テスト時のサンプルとユーザーを事前に有効化
- db/seeds.rb
- test/fixtures/users.yml
- アカウント有効化のメール送信
- Action Mailerライブラリを使ってUserのメイラーを追加
- 送信メールのテンプレート
- rails generate mailer UserMailer account_activation password_reset
- メイラーは、モデルやコントローラと同様にrails generateで生成
- 生成したメイラーごとに、ビューのテンプレートが2つずつ生成されます。1つはテキストメール用のテンプレート、1つはHTMLメール用のテンプレート
- 最初に、生成されたテンプレートをカスタマイズして、実際に有効化メールで使えるようにします
- app/mailers/application_mailer.rb
- default from: "noreply@example.com"
- app/mailers/user_mailer.rb
- def account_activation(user)
- mail to: user.email, subject: "Account activation"
- def account_activation(user)
- app/views/user_mailer/account_activation.text.erb
- アカウント有効化のテキストビュー
- <%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
- app/views/user_mailer/account_activation.html.erb
- アカウント有効化のHTMLビュー
- app/mailers/application_mailer.rb
- rails generate mailer UserMailer account_activation password_reset
- 送信メールのプレビュー
- 送信メールのテスト
- ユーザーのcreateアクションを更新
- アカウントを有効化する
- 今度はAccountActivationsコントローラのeditアクションを書いていきましょう
- アクションへのテストを書き、しっかりとテストできていることが確認できたら、AccountActivationsコントローラからUserモデルにコードを移していく作業 (リファクタリング)
- authenticated?メソッドの抽象化
- メタプログラミング
- sendメソッドの強力きわまる機能です。このメソッドは、渡されたオブジェクトに「メッセージを送る」ことによって、呼び出すメソッドを動的に決めることができます
- app/models/user.rb
- 抽象化されたauthenticated?メソッド
- digest = send("#{attribute}_digest")
- app/helpers/sessions_helper.rb
- if user && user.authenticated?(:remember, cookies[:remember_token])
- test/models/user_test.rb
- assert_not @user.authenticated?(:remember, '')
- メタプログラミング
- editアクションで有効化
- paramsハッシュで渡されたメールアドレスに対応するユーザーを認証
- if user && !user.activated? && user.authenticated?(:activation, params[:id])
- app/controllers/account_activations_controller.rb
- アカウントを有効化するeditアクション
- user.update_attribute(:activated, true)
- user.update_attribute(:activated_at, Time.zone.now)
- app/controllers/sessions_controller.rb
- 有効でないユーザーがログインすることのないようにする
- paramsハッシュで渡されたメールアドレスに対応するユーザーを認証
- 有効化のテストとリファクタリング
- test/integration/users_signup_test.rb
- assert_equal 1, ActionMailer::Base.deliveries.size
- 本当に重要な1点
- deliveriesは変数なので、setupメソッドでこれを初期化しておかないと、並行して行われる他のテストでメールが配信されたときにエラーが発生
- assert_equal 1, ActionMailer::Base.deliveries.size
- app/models/user.rb
- Userモデルにユーザー有効化メソッドを追加
- user.という記法を使っていない
- def activate
- update_attribute(:activated, true)
- update_attribute(:activated_at, Time.zone.now)
- def send_activation_email
- UserMailer.account_activation(self).deliver_now
- app/controllers/users_controller.rb
- ユーザーモデルオブジェクトからメールを送信
- @user.send_activation_email
- app/controllers/account_activations_controller.rb
- ユーザーモデルオブジェクト経由でアカウントを有効化
- user.activate
- 演習
- update_columns(activated: true, activated_at: Time.zone.now)
- @users = User.where(activated: true).paginate(page: params[:page])
- redirect_to root_url and return unless @user.activated?
- まずshow用にintegrationテストを作成する(indexは既存)
- rails g integration_test users_show
- テスト用のactivated: falseユーザーを新しく作る
- test/fixtures/users.yml
- activated: false
- test/integration/users_index_test.rb
- if user.activated?
- else
- assert_select 'a[href=?]', user_path(user), text: user.name, count: 0
- unless user == @admin || !user.activated?
- test/integration/users_show_test.rb
- get user_path(@non_activated_user)
- assert_redirected_to root_url
- test/integration/users_signup_test.rb
- 本番環境でのメール送信
- heroku apps:rename sample-app
- 本番環境からメール送信するために、「Mailgun」というHerokuアドオンを利用してアカウントを検証
- (このアドオンを利用するためにはHerokuアカウントにクレジットカードを設定する必要がありますが、アカウント検証では料金は発生しません)
- 「starter」というプランを使うことにします。これは、(執筆時点では) 1日のメール数が最大400通までという制限がありますが、無料で利用することができます
- config/environments/production.rb
- Railsのproduction環境でMailgunを使う設定
- heroku addons:create mailgun:starter
- MailgunのHerokuアドオンを追加するために、コマンドを実行
- Herokuの環境変数を表示したい場合は、次のコマンドを実行
- Mailgunアカウントのuser_nameとpassword設定を記入する行もありますが、そこには記入せず、必ず環境変数「ENV」に設定する
- 受信メールの認証
- heroku addons:open mailgun
- 最後に
- アカウント有効化を実装したことにより、サンプルアプリケーションの「ユーザー登録」「ログイン」「ログアウト」の仕組みがほぼ完成した
- これを完成させるための最後に1ピースは、ユーザーがパスワードを忘れた時の「パスワード再設定」機能
- 本章のまとめ
- アカウント有効化は Active Recordオブジェクトではないが、セッションの場合と同様に、リソースでモデル化できる
- Railsは、メール送信で扱うAction Mailerのアクションとビューを生成することができる
- Action MailerではテキストメールとHTMLメールの両方を利用できる
- メイラーアクションで定義したインスタンス変数は、他のアクションやビューと同様、メイラーのビューから参照できる
- アカウントを有効化させるために、生成したトークンを使って一意のURLを作る
- より安全なアカウント有効化のために、ハッシュ化したトークン (ダイジェスト) を使う
- メイラーのテストと統合テストは、どちらもUserメイラーの振舞いを確認するのに有用
- Mailgunを使うと、production環境からメールを送信できる
Ch12 パスワードの再設定
- ほとんどは、アカウント有効化で見てきた内容と似通っています
- PasswordResetsリソースを作成して、再設定用のトークンとそれに対応するダイジェストを保存するのが今回の目的
- 全体の流れ
- ユーザーがパスワードの再設定をリクエストすると、ユーザーが送信したメールアドレスをキーにしてデータベースからユーザーを見つける
- 該当のメールアドレスがデータベースにある場合は、再設定用トークンとそれに対応する再設定ダイジェストを生成する
- 再設定用ダイジェストはデータベースに保存しておき、再設定用トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
- ユーザーがメールのリンクをクリックしたら、メールアドレスをキーとしてユーザーを探し、データベース内に保存しておいた再設定用ダイジェストと比較する (トークンを認証する)
- 認証に成功したら、パスワード変更用のフォームをユーザーに表示する
- PasswordResetsリソース
- まずはPasswordResetsリソースのモデリング
- 新たなモデルは作らずに、代わりに必要なデータ (再設定用のダイジェストなど) をUserモデルに追加していく形
- PasswordResetsもリソースとして扱っていきたいので、まずは標準的なRESTfulなURLを用意
- パスワードを再設定するフォームが必要なので、ビューを描画するためのnewアクションとeditアクションが必要
- それぞれのアクションに対応する作成用/更新用のアクションも最終的なRESTfulなルーティングには必要になります
- PasswordResetsコントローラ
- rails generate controller PasswordResets new edit --no-test-framework
- config/routes.rb
- resources :password_resets, only: [:new, :create, :edit, :update]
- app/views/sessions/new.html.erb
- パスワード再設定画面へのリンクを追加する
- <%= link_to "(forgot password)", new_password_reset_path %>
- 新しいパスワードの設定
- パスワードの再設定でも、トークン用の仮想的な属性とそれに対応するダイジェストを用意
- 再設定用のリンクはなるべく短時間 (数時間以内) で期限切れになるようにしなければなりません
- reset_digest属性とreset_sent_at属性をUserモデルに追加
- rails generate migration add_reset_to_users reset_digest:string reset_sent_at:datetime
- app/views/password_resets/new.html.erb
- 新しいセッションを作成するためのログインフォーム (リスト 8.4) を使います
- form_forで扱うリソースとURLが異なっている点と、パスワード属性が省略されている点
- 演習
- password_resetはUserモデルに変更を加えるコントローラ
- インスタンス変数を使う場合@user
- form_forに@userを使うと/usersに対応するPOSTとしてsignupとなる
- → :password_reset
- password_resetはUserモデルに変更を加えるコントローラ
- createアクションでパスワード再設定
- app/controllers/password_resets_controller.rb
- パスワード再設定用のcreateアクション
- app/models/user.rb
- Userモデルにパスワード再設定用メソッドを追加する
- def create_reset_digest
- def send_password_reset_email
- app/controllers/password_resets_controller.rb
- まずはPasswordResetsリソースのモデリング
- パスワード再設定のメール送信
- パスワード再設定のメールとテンプレート
- 送信メールのテスト
- test/mailers/user_mailer_test.rb
- パスワード再設定用メイラーメソッドのテストを追加
- test/mailers/user_mailer_test.rb
- パスワードを再設定する
- editアクションで再設定
- app/views/password_resets/edit.html.erb
- パスワード再設定のフォーム
- <%= hidden_field_tag :email, @user.email %>
- editアクションとupdateアクションの両方でメールアドレスが必要
- 前者 (hidden_field_tag) ではメールアドレスがparams[:email]に保存されますが、後者(f.hidden_field :email, @user.email)ではparams[:user][:email] に保存されてしまう
- app/controllers/password_resets_controller.rb
- app/views/password_resets/edit.html.erb
- パスワードを更新する
- app/controllers/password_resets_controller.rb
- before_action :check_expiration, only: [:edit, :update]
- def update
- if params[:user][:password].empty?
- @user.errors.add(:password, :blank)
- elsif @user.update_attributes(user_params)
- else
- render 'edit'
- if params[:user][:password].empty?
- def user_params
- params.require(:user).permit(:password, :password_confirmation)
- def check_expiration
- if @user.password_reset_expired?
- app/models/user.rb
- def password_reset_expired?
- reset_sent_at < 2.hours.ago
- def password_reset_expired?
- app/controllers/password_resets_controller.rb
- パスワードの再設定をテストする
- rails generate integration_test password_resets
- test/integration/password_resets_test.rb
- パスワード再設定をテストする手順は、アカウント有効化のテスト (リスト 11.33) と多くの共通点がありますが、テストの冒頭部分には次のような違い
- 最初に「forgot password」フォームを表示して無効なメールアドレスを送信し、次はそのフォームで有効なメールアドレスを送信
- 後者ではパスワード再設定用トークンが作成され、再設定用メールが送信
- メールのリンクを開いて無効な情報を送信し、次にそのリンクから有効な情報を送信して、それぞれが期待どおりに動作
- assert_select "input[name=email][type=hidden][value=?]", user.email
- inputタグに正しい名前、type="hidden"、メールアドレスがあるかどうかを確認
- パスワード再設定をテストする手順は、アカウント有効化のテスト (リスト 11.33) と多くの共通点がありますが、テストの冒頭部分には次のような違い
- 演習
- def create_reset_digest
- update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
- assert_match /expired/i, response.body
- 期限切れをテストする方法
- レスポンスの本文に「expired」という語があるかどうかでチェック
- 期限切れをテストする方法
- @user.update_attribute(:reset_digest, nil)
- パスワードの再設定に成功したらダイジェストをnilになるように変更
- test/integration/password_resets_test.rb
- test "password resets" doの最後
- assert_nil user.reload.reset_digest
- test "password resets" doの最後
- def create_reset_digest
- editアクションで再設定
- 本番環境でのメール送信 (再掲)
- 最後に
- サンプルアプリケーションのユーザー登録・ログイン・ログアウトの仕組みは、本物のアプリケーションと近いレベルに仕上がりました
- 本章のまとめ
- パスワードの再設定は Active Recordオブジェクトではないが、セッションやアカウント有効化の場合と同様に、リソースでモデル化できる
- Railsは、メール送信で扱うAction Mailerのアクションとビューを生成することができる
- Action MailerではテキストメールとHTMLメールの両方を利用できる
- メイラーアクションで定義したインスタンス変数は、他のアクションやビューと同様、メイラーのビューから参照できる
- パスワードを再設定させるために、生成したトークンを使って一意のURLを作る
- より安全なパスワード再設定のために、ハッシュ化したトークン (ダイジェスト) を使う
- メイラーのテストと統合テストは、どちらもUserメイラーの振舞いを確認するのに有用
- Mailgunを使うとproduction環境からメールを送信できる
- 証明: 期限切れの比較
Ch13 ユーザーのマイクロポスト
- Micropostモデル
- まずはMicropostリソースの最も本質的な部分を表現するMicropostモデルを作成するところから
- 基本的なモデル
- Micropostモデルは、マイクロポストの内容を保存するcontent属性と、特定のユーザーとマイクロポストを関連付けるuser_id属性の2つの属性だけを持ちます
- String型ではなくText型
- ある程度の量のテキストを格納するとき
- 投稿フォームにString用のテキストフィールドではなくてText用のテキストエリアを使うため、より自然な投稿フォームが実現
- Text型の方が将来における柔軟性に富んでいて、例えばいつか国際化をするときに、言語に応じて投稿の長さを調節することもできます
- Text型を使っていても本番環境でパフォーマンスの差は出ません
- rails generate model Micropost content:text user:references
- add_index :microposts, [:user_id, :created_at]
- user_idに関連付けられたすべてのマイクロポストを作成時刻の逆順で取り出しやすくなります
- 両方のキーを同時に扱う複合キーインデックス (Multiple Key Index) を作成
- Micropostのバリデーション
- test/models/micropost_test.rb
- 新しいMicropostの有効性に対するテスト
- Micropostモデルのバリデーションに対するテスト
- app/models/micropost.rb
- validates :user_id, presence: true
- validates :content, presence: true, length: { maximum: 140 }
- test/models/micropost_test.rb
- User/Micropostの関連付け
- Webアプリケーション用のデータモデルを構築するにあたって、個々のモデル間での関連付けを十分考えておくことが重要
- 紐付いているユーザーを通してマイクロポストを作成
- @micropost = @user.microposts.build(content: "Lorem ipsum")
- newメソッドと同様に、buildメソッドはオブジェクトを返しますがデータベースには反映されません。
- @user.microposts.buildのようなコードを使うためには、 UserモデルとMicropostモデルをそれぞれ更新して、関連付ける必要
- @micropost = @user.microposts.build(content: "Lorem ipsum")
- app/models/micropost.rb
- belongs_to :user
- app/models/user.rb
- has_many :microposts
- test/models/micropost_test.rb
- @micropost = @user.microposts.build(content: "Lorem ipsum")
- マイクロポストを改良する
- ユーザーのマイクロポストを特定の順序で取得できるようにしたり、マイクロポストをユーザーに依存させて、ユーザーが削除されたらマイクロポストも自動的に削除されるようにしていきます
- デフォルトのスコープ
- test/models/micropost_test.rb
- assert_equal microposts(:most_recent), Micropost.first
- test/fixtures/microposts.yml
- マイクロポスト用のfixture
- created_atカラム
- Railsによって自動的に更新されるため基本的には手動で更新できない
- fixtureファイルの中では更新可能
- app/models/micropost.rb
- default_scope -> { order(created_at: :desc) }
- Procやlambda (もしくは無名関数)と呼ばれるオブジェクトを作成する文法
- ->というラムダ式は、ブロック (4.3.2) を引数に取り、Procオブジェクトを返します
- このオブジェクトは、callメソッドが呼ばれたとき、ブロック内の処理を評価
- default_scope -> { order(created_at: :desc) }
- test/models/micropost_test.rb
- Dependent: destroy
- app/models/user.rb
- マイクロポストは、その所有者 (ユーザー) と一緒に破棄されることを保証する
- has_many :microposts, dependent: :destroy
- test/models/user_test.rb
- assert_difference 'Micropost.count', -1 do
- @user.destroy
- assert_difference 'Micropost.count', -1 do
- app/models/user.rb
- マイクロポストを表示する
- マイクロポストの描画
- rails generate controller Microposts
- app/views/microposts/_micropost.html.erb
- 1つのマイクロポストを表示するパーシャル
- <\li id="micropost-<%= micropost.id %>">
- 将来、JavaScriptを使って各マイクロポストを操作したくなったときなどに役立ちます
- app/controllers/users_controller.rb
- @microposts = @user.microposts.paginate(page: params[:page])
- app/views/users/show.html.erb
- マイクロポストをユーザーのshowページ (プロフィール画面) に追加
- 順序無しリストのulタグではなく、順序付きリストのolタグを使っている
- マイクロポストが特定の順序 (新しい→古い) に依存しているため
- <%= will_paginate @microposts %>
- 明示的に@microposts変数をwill_paginateに渡す必要
- インスタンス変数をUsersコントローラのshowアクションで定義
- マイクロポストのサンプル
- db/seeds.rb
- サンプルデータにマイクロポストを追加
- content = Faker::Lorem.sentence(5)
- users.each { |user| user.microposts.create!(content: content) }
- ループの順序に違和感があるかもしれませんが、これは14.3でステータスフィード (いわゆるタイムライン) を実装するときに役立ちます
- app/assets/stylesheets/custom.scss
- マイクロポスト用のCSS
- db/seeds.rb
- プロフィール画面のマイクロポストをテストする
- rails generate integration_test users_profile
- test/fixtures/microposts.yml
- ユーザーと関連付けされたマイクロポストのfixture
- <% 30.times do |n| %>
- micropost_<%= n %>:
- test/integration/users_profile_test.rb
- 今回のテストでは、プロフィール画面にアクセスした後に、ページタイトルとユーザー名、Gravatar、マイクロポストの投稿数、そしてページ分割されたマイクロポスト、といった順でテスト
- assert_match @user.microposts.count.to_s, response.body
- assert_selectよりもずっと抽象的なメソッド
- assert_selectではどのHTMLタグを探すのか伝える必要がありますが、assert_matchメソッドではその必要がない
- assert_select 'h1>img.gravatar'
- ネストした文法
- h1タグ (トップレベルの見出し) の内側にある、gravatarクラス付きのimgタグがあるかどうかをチェック
- 演習
- assert_select 'div.pagination', count: 1
- マイクロポストの描画
- マイクロポストを操作する
- データモデリングとマイクロポスト表示テンプレートの両方が完成したので、次はWeb経由でそれらを作成するためのインターフェイスに取りかかりましょう
- Micropostsリソースへのインターフェイスは、主にプロフィールページとHomeページのコントローラを経由して実行
- Micropostsコントローラにはnewやeditのようなアクションは不要
- createとdestroyがあれば十分
- config/routes.rb
- resources :microposts, only: [:create, :destroy]
- scaffoldが生成するような複雑なコードはほとんど不要になりました
- マイクロポストのアクセス制御
- test/controllers/microposts_controller_test.rb
- Micropostsコントローラの認可テスト
- ログイン済みかどうかを確かめるテストでは、Usersコントローラ用のテストがそのまま役に立ちます
- 正しいリクエストを各アクションに向けて発行し、マイクロポストの数が変化していないかどうか、また、リダイレクトされるかどうかを確かめればよい
- test "should redirect create when not logged in" do
- test "should redirect destroy when not logged in" do
- Micropostsコントローラの認可テスト
- app/controllers/application_controller.rb
- logged_in_userメソッドをApplicationコントローラに移す
- app/controllers/users_controller.rb
- Usersコントローラ内のlogged_in_userフィルターを削除
- app/controllers/microposts_controller.rb
- before_action :logged_in_user, only: [:create, :destroy]
- test/controllers/microposts_controller_test.rb
- マイクロポストを作成する
- ユーザーのサインアップを実装しました。マイクロポスト作成の実装もこれと似ています
- 主な違いは、別の micropost/new ページを使う代わりに、ホーム画面 (つまりルートパス) にフォームを置くという点
- ユーザーのログイン状態に応じて、ホーム画面の表示を変更
- app/controllers/microposts_controller.rb
- Micropostsコントローラのcreateアクション
- ユーザー用アクションと似ています
- 違いは、新しいマイクロポストをbuildするためにUser/Micropost関連付けを使っている点
- micropost_paramsでStrong Parametersを使っていることにより、マイクロポストのcontent属性だけがWeb経由で変更可能になっている
- params.require(:micropost).permit(:content)
- app/views/static_pages/home.html.erb
- Homeページ (/) にマイクロポストの投稿フォームを追加する
- <% if logged_in? %>
- <%= render 'shared/user_info' %>
- <%= render 'shared/micropost_form' %>
- app/views/shared/_user_info.html.erb
- サイドバーで表示するユーザー情報のパーシャル
- app/views/shared/_micropost_form.html.erb
- マイクロポスト投稿フォームのパーシャル
- app/controllers/static_pages_controller.rb
- @micropost = current_user.microposts.build if logged_in?
- app/views/shared/_error_messages.html.erb
- Userオブジェクト以外でも動作するようにerror_messagesパーシャルを更新する
- @userをobjectにする
- app/views/users/_form.html.erb
- @userをf.objectにする
- app/views/password_resets/edit.html.erb
- <%= render 'shared/error_messages', object: f.object %>
- vim test/integration/site_layout_test.rb
- 修正必要
- 演習
- app/views/static_pages/_home_logged_in.html.erb
- app/views/static_pages/_home_not_logged_in.html.erb
- フィードの原型
- すべてのユーザーがフィードを持つので、feedメソッドはUserモデルで作るのが自然
- app/models/user.rb
- マイクロポストのステータスフィードを実装するための準備
- Micropost.where("user_id = ?", id)
- 上の疑問符があることで、SQLクエリに代入する前にidがエスケープされるため、SQLインジェクション (SQL Injection) と呼ばれる深刻なセキュリティホールを避けることができます
- app/controllers/static_pages_controller.rb
- homeアクションにフィードのインスタンス変数を追加する
- @feed_items = current_user.feed.paginate(page: params[:page])
- app/views/shared/_feed.html.erb
- ステータスフィードのパーシャル
- <%= render @feed_items %>
- <%= will_paginate @feed_items %>
- app/views/static_pages/home.html.erb
- Homeページにステータスフィードを追加する
- <%= render 'shared/feed' %>
- app/controllers/microposts_controller.rb
- createアクションに空の@feed_itemsインスタンス変数を追加
- マイクロポストを削除する
- app/views/microposts/_micropost.html.erb
- マイクロポストのパーシャルに削除リンクを追加する
- <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %>
- app/controllers/microposts_controller.rb
- Micropostsコントローラのdestroyアクション
- ユーザーにおける実装 (リスト 10.59) とだいたい同じ
- 大きな違いは、admin_userフィルターで@user変数を使うのではなく、関連付けを使ってマイクロポストを見つけるようにしている点
- destroyメソッドではリダイレクトを使っている
- request.referrer || root_url
- request.referrer
- 一つ前のURLを返します
- request.referrerを使うことでDELETEリクエストが発行されたページに戻すことができるので、非常に便利
- redirect_back(fallback_location: root_url)と置き換えてもうまく動く
- request.referrer
- request.referrer || root_url
- app/views/microposts/_micropost.html.erb
- フィード画面のマイクロポストをテストする
- test/fixtures/microposts.yml
- 別のユーザーに所属しているマイクロポストを追加
- test/controllers/microposts_controller_test.rb
- 間違ったユーザーによるマイクロポスト削除に対してテスト
- log_in_as(users(:michael))
- micropost = microposts(:ants)
- assert_no_difference 'Micropost.count' do
- delete micropost_path(micropost)
- rails generate integration_test microposts_interface
- test/integration/microposts_interface_test.rb
- 演習
- test/integration/microposts_interface_test.rb
- assert_match "#{@user.microposts.count} microposts", response.body
- assert_match "1 micropost", response.body
- test/integration/microposts_interface_test.rb
- test/fixtures/microposts.yml
- マイクロポストの画像投稿
- 基本的な画像アップロード
- CarrierWaveという画像アップローダー
- Rails 5.2から標準となったActive Storage
- GemfileにCarrierWaveを追加する
- mini_magick gemとfog gemsも含めている
- CarrierWaveを導入すると、Railsのジェネレーターで画像アップローダーが生成できるようになります
- rails generate uploader Picture
- CarrierWaveでアップロードされた画像は、Active Recordモデルの属性と関連付けされているべき
- 関連付けされる属性には画像のファイル名が格納されるため、String型
- rails generate migration add_picture_to_microposts picture:string
- app/models/micropost.rb
- Micropostモデルに画像を追加する
- mount_uploader :picture, PictureUploader
- app/views/shared/_micropost_form.html.erb
- app/controllers/microposts_controller.rb
- params.require(:micropost).permit(:content, :picture)
- app/views/microposts/_micropost.html.erb
- マイクロポストの画像表示を追加
- <%= image_tag micropost.picture.url if micropost.picture? %>
- picture?という論理値を返すメソッド
- 画像用の属性名に応じて、CarrierWaveが自動的に生成してくれるメソッド
- picture?という論理値を返すメソッド
- 演習
- CarrierWaveという画像アップローダー
- 画像の検証
- app/uploaders/picture_uploader.rb
- app/models/micropost.rb
- 画像に対するバリデーションを追加
- validate :picture_size
- 独自のバリデーションを定義するために、今まで使っていたvalidatesメソッドではなく、validateメソッドを使っている
- ファイルサイズに対するバリデーションはRailsの既存のオプション (presenceやlengthなど) にはありません
- def picture_size
- if picture.size > 5.megabytes
- errors.add(:picture, "should be less than 5MB")
- if picture.size > 5.megabytes
- app/views/shared/_micropost_form.html.erb
- ファイルサイズをjQueryでチェック
- <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
- <\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.');
- $('#micropost_picture').bind('change', function() {
- ユーザーはアラートを無視してアップロードを強行する、といったことが可能
- 画像のリサイズ
- sudo apt install -y ImageMagick
- MiniMagickというImageMagickとRubyを繋ぐgemを使って、画像をリサイズ
- app/uploaders/picture_uploader.rb
- 画像をリサイズするために画像アップローダーを修正
- include CarrierWave::MiniMagick
- process resize_to_limit: [400, 400]
- storage :file
- def store_dir
- "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
- 演習
- config/initializers/skip_image_resizing.rb
- テスト時は画像のリサイズをさせない設定
- config/initializers/skip_image_resizing.rb
- 本番環境での画像アップロード
- 基本的な画像アップロード
- 最後に
- 本章のまとめ
- Active Recordモデルの力によって、マイクロポストも (ユーザーと同じで) リソースとして扱える
- Railsは複数のキーインデックスをサポートしている
- Userは複数のMicropostsを持っていて (has_many)、Micropostは1人のUserに依存している (belongs_to) といった関係性をモデル化した
- has_manyやbelongs_toを利用することで、関連付けを通して多くのメソッドが使えるようになった
- user.microposts.build(...)というコードは、引数で与えたユーザーに関連付けされたマイクロポストを返す
- default_scopeを使うとデフォルトの順序を変更できる
- default_scopeは引数に無名関数 (->) を取る
- dependent: :destroyオプションを使うと、関連付けされたオブジェクトと自分自身を同時に削除する
- paginateメソッドやcountメソッドは、どちらも関連付けを通して実行され、効率的にデータベースに問い合わせしている
- fixtureは、関連付けを使ったオブジェクトの作成もサポートしている
- パーシャルを呼び出すときに、一緒に変数を渡すことができる
- whereメソッドを使うと、Active Recordを通して選択 (部分集合を取り出すこと) ができる
- 依存しているオブジェクトを作成/削除するときは、常に関連付けを通すようにすることで、よりセキュアな操作が実現できる
- CarrierWaveを使うと画像アップロードや画像リサイズができる
- 本章のまとめ
Ch14 ユーザーをフォローする
- 他のユーザーをフォロー (およびフォロー解除) できるソーシャルな仕組みの追加と、フォローしているユーザーの投稿をステータスフィードに表示する機能を追加
- Relationshipモデル
- ユーザーをフォローする機能を実装する第一歩は、データモデルを構成すること
- データモデルの問題 (および解決策)
- followingテーブルと has_many関連付けを使って、フォローしているユーザーのモデリング
- 正しいモデルを見つけ出す方法の1つは、Webアプリケーションにおけるfollowingの動作をどのように実装するかをじっくり考えること
- まずは、フォローしているユーザーを生成するために、能動的関係に焦点を当てていきます
- followingテーブルをactive_relationshipsテーブルと見立ててみましょう
- 能動的関係も受動的関係も、最終的にはデータベースの同じテーブルを使うことになります。したがって、テーブル名にはこの「関係」を表す「relationships」を使いましょう
- rails generate model Relationship follower_id:integer followed_id:integer
- db/migrate/[timestamp]_create_relationships.rb
- relationshipsテーブルにインデックスを追加
- add_index :relationships, :follower_id
- add_index :relationships, :followed_id
- add_index :relationships, [:follower_id, :followed_id], unique: true
- あるユーザーが同じユーザーを2回以上フォローすることを防ぎます
- User/Relationshipの関連付け
- フォローしているユーザーとフォロワーを実装する前に、UserとRelationshipの関連付けを行います
- app/models/user.rb
- 能動的関係に対して1対多 (has_many) の関連付けを実装
- has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
- app/models/relationship.rb
- リレーションシップ/フォロワーに対してbelongs_toの関連付けを追加
- belongs_to :follower, class_name: "User"
- belongs_to :followed, class_name: "User"
- Relationshipのバリデーション
- 先に進む前に、Relationshipモデルの検証を追加して完全なものにしておきましょう
- test/models/relationship_test.rb
- Relationshipモデルのバリデーションをテスト
- def setup
- test "should be valid" do
- test "should require a follower_id" do
- test "should require a followed_id" do
- app/models/relationship.rb
- Relationshipモデルに対してバリデーションを追加
- Rails 5から必須ではなくなりました
- validates :follower_id, presence: true
- validates :followed_id, presence: true
- Relationshipモデルに対してバリデーションを追加
- test/fixtures/relationships.yml
- Relationship用のfixtureを空にする
- フォローしているユーザー
- Relationshipの関連付けの核心、followingとfollowersに取りかかります
- 今回はhas_many throughを使います
- app/models/user.rb
- Userモデルにfollowingの関連付けを追加する
- has_many :following, through: :active_relationships, source: :followed
- :sourceパラメーター (リスト 14.8) を使って、「following配列の元はfollowed idの集合である」ということを明示的にRailsに伝えます
- フォローしているユーザーを配列の様に扱えるようになりました
- followingメソッドで配列のように扱えるだけでも便利ですが、Railsは単純な配列ではなく、もっと賢くこの集合を扱っています
- following.include?(other_user)
- 実際にはデータベースの中で直接比較をするように配慮
- following.include?(other_user)
- 次に、followingで取得した集合をより簡単に取り扱うために、followやunfollowといった便利メソッドを追加
- 今回は、こういったメソッドはテストから先に書いていきます
- Webインターフェイスなどで便利メソッドを使うのはまだ先なので、すぐに使える場面がなく、実装した手応えを得にくいから
- 一方で、Userモデルに対するテストを書くのは簡単かつ今すぐできます
- test/models/user_test.rb
- “following” 関連のメソッドをテスト
- app/models/user.rb
- "following" 関連のメソッド
- 可能な限りself (user自身を表すオブジェクト) を省略している
- def follow(other_user)
- following << other_user
- def unfollow(other_user)
- active_relationships.find_by(followed_id: other_user.id).destroy
- def following?(other_user)
- following.include?(other_user)
- 今回は、こういったメソッドはテストから先に書いていきます
- フォロワー
- リレーションシップというパズルの最後の一片は、user.followersメソッドを追加すること
- user.followingメソッドと対
- app/models/user.rb
- 受動的関係を使ってuser.followersを実装
- この実装はリスト 14.8とまさに類似
- 参照先 (followers) を指定するための:sourceキーを省略してもよかった
- :followers属性の場合、Railsが「followers」を単数形にして自動的に外部キーfollower_idを探してくれるから
- test/models/user_test.rb
- followersに対するテスト
- assert archer.followers.include?(michael)を追加
- リレーションシップというパズルの最後の一片は、user.followersメソッドを追加すること
- [Follow] のWebインターフェイス
- この節では、モックアップで示したようにフォロー/フォロー解除の基本的なインターフェイスを実装
- また、フォローしているユーザーと、フォロワーにそれぞれ表示用のページを作成
- フォローのサンプルデータ
- 先にサンプルデータを自動作成できるようにしておけば、Webページの見た目のデザインから先にとりかかることができ、バックエンド機能の実装を後に回すことができます
- db/seeds.rb
- サンプルデータにfollowing/followerの関係性を追加
- users = User.all
- user = users.first
- following = users[2..50]
- followers = users[3..40]
- following.each { |followed| user.follow(followed) }
- followers.each { |follower| follower.follow(user) }
- 統計と [Follow] フォーム
- config/routes.rb
- Usersコントローラにfollowingアクションとfollowersアクションを追加する
- resources :users do
- member do
- get :following, :followers
- member do
- この場合のURLは /users/1/following や /users/1/followers のようになる
- memberメソッドを使うとユーザーidが含まれているURLを扱うようになります
- idを指定せずにすべてのメンバーを表示するには、次のようにcollectionメソッド
- collection do
- get :rabbits
- /users/rabbitsに対応
- collection do
- ルーティングを定義したので、統計情報のパーシャルを実装する準備が整いました
- app/views/shared/_stats.html.erb
- フォロワーの統計情報を表示するパーシャル
- このパーシャルでは、divタグの中に2つのリンクを含めるようにします
- <% @user ||= current_user %>
- このパーシャルはプロフィールページとHomeページの両方に表示
- @user.following.count
- @user.followers.count
- <\strong id="following" class="stat">
- こうしておくと、14.2.5でAjaxを実装するときに便利
- 一意のidを指定してページ要素にアクセス
- app/views/static_pages/_home_logged_in.html.erb
- Homeページにフォロワーの統計情報を追加する
- <%= render 'shared/stats' %>
- app/assets/stylesheets/custom.scss
- Homeページのサイドバー用のSCSS
- app/views/users/_follow_form.html.erb
- フォロー/フォロー解除フォームのパーシャル
- <% unless current_user?(@user) %>
- <% if current_user.following?(@user) %>
- <%= render 'unfollow' %>
- <% else %>
- <%= render 'follow' %>
- config/routes.rb
- Relationshipリソース用のルーティングを追加する
- resources :relationships, only: [:create, :destroy]
- app/views/users/_follow.html.erb
- ユーザーをフォローするフォーム
- <%= form_for(current_user.active_relationships.build) do |f| %>
- <%= hidden_field_tag :followed_id, @user.id %>
- <%= f.submit "Follow", class: "btn btn-primary" %>
- app/views/users/_unfollow.html.erb
- ユーザーをフォロー解除するフォーム
- <%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }) do |f| %>
- <%= f.submit "Unfollow", class: "btn" %>
- これらの2つのフォームの主な違いは、リスト 14.21は新しいリレーションシップを作成するのに対し、リスト 14.22は既存のリレーションシップを見つけ出すという点
- app/views/users/show.html.erb
- プロフィールページにフォロー用フォームとフォロワーの統計情報を追加
- <%= render 'shared/stats' %>
- <%= render 'follow_form' if logged_in? %>
- 演習
- test/integration/site_layout_test.rb
- test/integration/users_profile_test.rb
- assert_match @user.active_relationships.count.to_s, response.body
- assert_match @user.passive_relationships.count.to_s, response.body
- [Following] と [Followers] ページ
- test/controllers/users_controller_test.rb
- フォロー/フォロワーページの認可をテスト
- 前回のアクセス制御と同様に、まずはテストから
- app/controllers/users_controller.rb
- app/views/users/show_follow.html.erb
- フォローしているユーザーとフォロワーの両方を表示するshow_followビュー
- 現在のユーザーを一切使っていない
- 他のユーザーのフォロワー一覧ページもうまく動きます
- rails generate integration_test following
- HTML構造を網羅的にチェックするテストは壊れやすく、生産性を逆に落としかねない
- 基本的なテストだけに留めており、網羅的なテストにはしていません
- 今回は、正しい数が表示されているかどうかと、正しいURLが表示されているかどうかの2つのテストを書きます
- test/fixtures/relationships.yml
- following/followerをテストするためのリレーションシップ用fixture
- test/integration/following_test.rb
- following/followerページのテスト
- assert_not @user.following.empty?
- 次のコードを確かめるためのテスト
- @user.following.each do |user|
- assert_select "a[href=?]", user_path(user)
- もし@user.following.empty?の結果がtrueであれば、assert_select内のブロックが実行されなくなるため、その場合においてテストが適切なセキュリティモデルを確認できなくなることを防いでいます
- [Follow] ボタン (基本編)
- rails generate controller Relationships
- フォローとフォロー解除はそれぞれリレーションシップの作成と削除に対応しているため、まずはRelationshipsコントローラが必要
- 最初にテストを書き、それをパスするように実装することでセキュリティモデルを確立
- test/controllers/relationships_controller_test.rb
- リレーションシップの基本的なアクセス制御に対するテスト
- コントローラのアクションにアクセスするとき、ログイン済みのユーザーであるかどうかをチェック
- もしログインしていなければログインページにリダイレクトされるので、Relationshipのカウントが変わっていないことを確認
- app/controllers/relationships_controller.rb
- logged_in_userフィルターをRelationshipsコントローラのアクションに対して追加
- リレーションシップのアクセス制御
- Relationshipsコントローラ
- フォーム (リスト 14.21とリスト 14.22) から送信されたパラメータを使って、followed_idに対応するユーザーを見つけてくる必要
- その後、見つけてきたユーザーに対して適切にfollow/unfollowメソッド (リスト 14.10) を使います
- [Follow] ボタン (Ajax編)
- Ajaxを使えば、Webページからサーバーに「非同期」で、ページを移動することなくリクエストを送信することができます
- form_forをform_for ..., remote: trueに置き換えるだけ
- app/views/users/_follow.html.erb
- <%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
- app/views/users/_unfollow.html.erb
- <%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }, remote: true) do |f| %>
- JavaScriptを前面に出すべからずという哲学
- 対応するRelationshipsコントローラを改造して、Ajaxリクエストに応答できるようにしましょう
- リクエストの種類によって応答を場合分けするときは、respond_toメソッド
- app/controllers/relationships_controller.rb
- ブラウザ側でJavaScriptが無効になっていた場合 (Ajaxリクエストが送れない場合) でもうまく動くようにします
- config/application.rb
- config.action_view.embed_authenticity_token_in_remote_forms = true
- Ajaxリクエストを受信した場合は、Railsが自動的にアクションと同じ名前を持つJavaScript用の埋め込みRuby (.js.erb) ファイル (create.js.erbやdestroy.js.erbなど) を呼び出す
- これらのファイルではJavaScriptと埋め込みRuby (ERb) をミックスして現在のページに対するアクションを実行する
- JS-ERbファイルの内部では、DOM (Document Object Model) を使ってページを操作するため、RailsがjQuery JavaScriptヘルパーを自動的に提供
- 純粋なJavaScriptと異なり、JS-ERbファイルでは組み込みRuby (ERb) が使えます
- app/views/relationships/create.js.erb
- JavaScriptと埋め込みRubyを使ってフォローの関係性を作成する
- $("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
- $("#followers").html('<%= @user.followers.count %>');
- app/views/relationships/destroy.js.erb
- Ruby JavaScript (RJS) を使ってフォローの関係性を削除する
- $("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
- $("#followers").html('<%= @user.followers.count %>');
- フォローをテストする
- test/integration/following_test.rb
- [Follow] / [Unfollow] ボタンをテスト
- assert_difference '@user.following.count', 1 do
- post relationships_path, params: { followed_id: @other.id }, xhr: true
- xhr (XmlHttpRequest) というオプションをtrueに設定すると、Ajaxでリクエストを発行するように変わります
- assert_difference '@user.following.count', -1 do
- delete relationship_path(relationship), xhr: true
- それぞれ従来どおりのテストと、Ajax用のテストの2つ
- ステータスフィード
- 現在のユーザーにフォローされているユーザーのマイクロポストの配列を作成し、現在のユーザー自身のマイクロポストと合わせて表示
- このセクションを通して、複雑さを増したフィードの実装に進んでいきます
- これを実現するためには、RailsとRubyの高度な機能の他に、SQLプログラミングの技術も必要
- 動機と計画
- この目的は、現在のユーザーによってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出す
- まずはテストから書いていく
- このテストで重要なことは、フィードに必要な3つの条件を満たすこと
- 1) フォローしているユーザーのマイクロポストがフィードに含まれている
- 2) 自分自身のマイクロポストもフィードに含まれている
- 3) フォローしていないユーザーのマイクロポストがフィードに含まれていない
- このテストで重要なことは、フィードに必要な3つの条件を満たすこと
- test/models/user_test.rb
- ステータスフィードのテスト
- assert michael.feed.include?(post_following)
- assert michael.feed.include?(post_self)
- assert_not michael.feed.include?(post_unfollowed)
- フィードを初めて実装する
- micropostsテーブルから、あるユーザー (つまり自分自身) がフォローしているユーザーに対応するidを持つマイクロポストをすべて選択 (select) する
- SQLがINというキーワードをサポートしていることを前提にしています (大丈夫、実際にサポートされています)。このキーワードを使うことで、idの集合の内包 (set inclusion) に対してテストを行えます
- フォローされているユーザーに対応するidの配列が必要
- これを行う方法の1つは、Rubyのmapメソッドを使うことです。このメソッドはすべての「列挙可能 (enumerable)」なオブジェクト (配列やハッシュなど、要素の集合で構成されるあらゆるオブジェクト) で使えます
- [1, 2, 3, 4].map(&:to_s)
- アンパサンド (Ampersand) & と、メソッドに対応するシンボルを使った短縮表記 (4.3.2) が使えます
- [1, 2, 3, 4].map(&:to_s).join(', ')
- User.first.following_ids
- following_idsメソッドは、has_many :followingの関連付けをしたときにActive Recordが自動生成したもの
- user.followingコレクションに対応するidを得るためには、関連付けの名前の末尾に_idsを付け足すだけで済みます
- User.first.following_ids.join(', ')
- 実際にSQL文字列に挿入するときは、このように記述する必要はありません。実は、?を内挿すると自動的にこの辺りの面倒を見てくれます
- さらに、データベースに依存する一部の非互換性まで解消
- Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id) ← これでOK
- app/models/user.rb
- とりあえず動くフィードの実装
- Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
- サブセレクト
- フォローしているユーザー数に応じてスケールできるように、ステータスフィードを改善
- app/models/user.rb
- Micropost.where("user_id IN (:following_ids) OR user_id = :user_id, following_ids: following_ids, user_id: id)
- 疑問符を使った文法も便利ですが、同じ変数を複数の場所に挿入したい場合は、置き換え後の文法を使う方がより便利
- following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id"
- Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id)
- 演習
- test/integration/following_test.rb
- assert_match CGI.escapeHTML(micropost.content), response.body
- test/integration/following_test.rb
- 最後に
- ステータスフィードが追加され、Railsチュートリアルのサンプルアプリケーションがとうとう完成しました
- このサンプルアプリケーションには、Railsの主要な機能 (モデル、ビュー、コントローラ、テンプレート、パーシャル、beforeフィルター、バリデーション、コールバック、has_many/belongs_to/has_many through関連付け、セキュリティ、テスティング、デプロイ) が多数含まれています
- サンプルアプリケーションの機能を拡張する
- この項で提案する拡張機能 (検索、返信、メッセージ機能など) のほとんどは、Webアプリケーションでは一般的な機能
- 一般的なアドバイス
- Ruby/Railsのバージョンを上げてみよう
- 返信機能
- @replyは受信者のフィードと送信者のフィードにのみ表示されるようにします
- これを実装するには、micropostsテーブルのin_reply_toカラムと、追加のincluding_repliesスコープをMicropostモデルに追加する必要がある
- スコープの詳細については、RailsガイドのActive Record クエリインターフェイスを参照
- このサンプルアプリケーションではユーザー名が重なり得るので、ユーザー名を一意に表す方法も考えなければならない
- 1つの方法は、idと名前を組み合わせて@1-michael-hartlのようにすること
- もう1つの方法は、ユーザー登録の項目に一意のユーザー名を追加し、@replyで使えるようにすること
- メッセージ機能
- ダイレクトメッセージを行える機能
- Messageモデルと、新規マイクロポストにマッチする正規表現が必要
- フォロワーの通知
- ユーザーに新しくフォロワーが増えたときにメールで通知する機能
- メールでの通知機能をオプションとして選択可能にし、不要な場合は通知をオフにできるようにしてみましょう
- メール周りで分からないことがあったら、RailsガイドのAction Mailerの基礎にヒントがないか調べてみましょう
- RSSフィード
- REST API
- 検索機能
- ユーザーを検索
- マイクロポストを検索
- まずは自分自身で検索機能に関する情報を探してみましょう。難しければ、@budougumi0617 さんの簡単な検索フォームの実装例を参考にしてください
- 他の拡張機能
- 読み物ガイド
- 本章のまとめ
- has_many :throughを使うと、複雑なデータ関係をモデリングできる
- has_manyメソッドには、クラス名や外部キーなど、いくつものオプションを渡すことができる
- 適切なクラス名と外部キーと一緒にhas_many/has_many :throughを使うことで、能動的関係 (フォローする) や受動的関係 (フォローされる) がモデリングできた
- ルーティングは、ネストさせて使うことができる
- whereメソッドを使うと、柔軟で強力なデータベースへの問い合わせが作成できる
- Railsは (必要に応じて) 低級なSQLクエリを呼び出すことができる
- 本書で学んだすべてを駆使することで、フォローしているユーザーのマイクロポスト一覧をステータスフィードに表示させることができた
- あとがき
おまけ
Ch15 React
- webpackの導入
- Webpacker gemの追加
- Asset Pipelineでも良いのではと思われると思いますが、webpackを利用すると他にも色々な機能が利用できます
- JavaScript以外で書かれたコードを、ブラウザが解釈できるJavaScriptに変換してあげる必要があり、その役割もwebpackが担ってくれます
- Reactなどをnodeモジュールとして管理したいという場合にも、webpackを利用することで簡単に管理できるようになります
- gem 'webpacker', '~> 3.0'
- Asset Pipelineでも良いのではと思われると思いますが、webpackを利用すると他にも色々な機能が利用できます
- webpackerのインストール
- rails webpacker:install
- webpackerのインストールタスクで生成されたファイルの説明
- git status
- package.json
- .babelrc
- app/javascript/*
- bin/*
- config/*
- webpackの利用
- app/javascript/packs/application.jsをload
- bin/webpackでtranspile
- トランスパイルされたファイルの読み込み
- app/views/layouts/_head.html.erb
- <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
- webpack-dev-serverを立ち上げておくことで、ファイルの変更の度にコンソールから$ bin/webpackを実行しなくても自動でトランスパイルが実行
- app/views/layouts/_head.html.erb
- Webpacker gemの追加
- Reactの導入
- Reactのインストール
- rails webpacker:install:react
- bin/webpack
- Reactコンポーネントの利用
- app/views/layouts/_head.html.erb
- <%= javascript_pack_tag 'hello_react', 'data-turbolinks-track': 'reload' %>
- ReactDOM.renderは2つの引数をとり、1つ目の引数がコンポーネント、2つ目の引数がそのコンポーネントが表示されるDOM要素
- app/views/layouts/_head.html.erb
- Reactコンポーネントを自由に表示する
- react-railsというgemを導入
- rails generate react:install
- spring stopしてからやらないとエラーになった
- 依存するライブラリがインストールされたり、app/javascript/packs/application.jsに新しくreact-railsの設定が追記
- Helloコンポーネントをapp/javascripts/components/hello_react.jsxに移動
- ReactDOM.render()はreact-railsが実行してくれるのでコメントアウトして、export default Helloを追記
- export default Hello
- app/javascript/packs/application.jsに追記されたreact-railsの設定で、コンポーネントを明示的にimportする必要はありません
- react-railsが用意したヘルパメソッドに、app/javascripts/components以下に置かれたコンポーネントのファイル名を指定してあげることでインポート
- app/views/layouts/_head.html.erb
- hello_reactの行を削除
- app/views/layouts/application.html.erb
- <%= react_component("hello_react", name: "React") %>
- Reactのインストール
- カウンターコンポーネントを作る
- app/javascript/components/counter.jsx
- app/views/layouts/application.html.erb
- <%= react_component("counter") %>
- フォローボタンのコンポーネントを作る
- フォローボタンのコンポーネント雛形作成
- app/views/users/_follow_form.html.erb
- <%= react_component("follow_button", user: @user.attributes, relationship: current_user.active_relationships.find_by(followed_id: @user.id)&.attributes) %>
- app/javascript/components/follow_button.jsx
- app/views/users/_follow_form.html.erb
- コンポーネントから通信する
- チャタリングを防止する
- リクエスト中は、ボタンの操作を行えないように、stateにリクエスト中のフラグを用意
- app/javascript/components/follow_button.jsx
- constructor(props) {
- this.state = {
- loading: false,
- this.state = {
- follow = () => { // unfollowも同じ
- loading: true
- }).then((response) => {
- loading: false,
- <\button className={ className } onClick={ this.handleClickFollowButton } disabled={ this.state.loading }>
- constructor(props) {
- ボタンの見た目を良くする
- yarn add classnames
- classnamesというモジュールを導入して、状態によるクラス名の変更を簡単にしましょう
- yarn add classnames
- フォローボタンのコンポーネント雛形作成
- Reduxの紹介
- Reduxとは
- 準備
- yarn add redux react-redux
- Reduxでカウンターを作成する
- Action
- ActionCreator
- Reducer
- Store
- ContainerComponent
- Component
- Storeとコンポーネントを接続する
- Storeを渡すのはContainerComponentではなく普通のComponentがいい
- 最後に
- 本章のまとめ
- webpackergemを利用することで、簡単にReactを利用した開発を行える
- JavaScriptは進化し続けていて、最新の記法で記述したJavaScriptをブラウザが理解できる形式に変換するためにトランスパイラを利用する ― JavaScriptの最新の記法は謎めいているが、慣れると非常に協力である
- Reactを利用すると、HTMLとJSをコンポーネントという単位でまとめるためて記述することができる
- Reactは渡されるpropsの変化や内部のstateが更新される度にrenderを呼び出し、propsやstateに応じたHTMLを返す
- JavaScriptからAjax通信を行う場合は、CSRFトークンをリクエストに含める必要がある
- ReactコンポーネントのHTMLのクラスを条件によって変えたい場合は、classnamesを利用する
- Reduxは登場人物(ファイル)が多いだけで、各登場人物の役割は単純である
- Reduxを利用するとstateがStoreで一元管理されるので、状態の見通しが良くなる
- Reduxは必ず利用する必要はなく、コンポーネントが複雑なstateを持ち、それらが他のコンポーネントにも影響を与える場合に利用を検討すべき
- 本章のまとめ
- <% if current_user.following?(@user) %>
- config/routes.rb