はじめに
Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 10章 10.4 ユーザーを削除するの演習まとめ&解答例です。
個人の解答例なので、誤りがあればご指摘ください。
動作環境
- cloud9
- ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
- Rails 5.0.0.1
10.4.1 管理ユーザー
本章での学び
ユーザーを削除することができる、管理ユーザーを作成する。
【model】usersにadmin属性を追加
管理ユーザーかどうかを判定する、admin属性を追加する。
追加することで自動的にadmin?
メソッドも使えるようになる。
マイグレーションを実行して、usersテーブルにadminカラムを追加する、マイグレーションファイルを生成する。
yokoyan:~/workspace/sample_app (updating-users) $ rails generate migration add_admin_to_users admin:boolean Running via Spring preloader in process 2467 Expected string default value for '--jbuilder'; got true (boolean) invoke active_record create db/migrate/20170614215124_add_admin_to_users.rb
自動生成されたマイグレーションファイルを修正する。
default:false
とすることで、デフォルトは管理者ではないことを示す。
class AddAdminToUsers < ActiveRecord::Migration[5.0] def change add_column :users, :admin, :boolean, default:false end end
マイグレーションファイルを実行する。
yokoyan:~/workspace/sample_app (updating-users) $ rails db:migrate == 20170614215124 AddAdminToUsers: migrating ================================== -- add_column(:users, :admin, :boolean, {:default=>false}) -> 0.0087s == 20170614215124 AddAdminToUsers: migrated (0.0088s) =========================
【modele】動作確認
sandboxでconsoleを開き、動作確認を行う。
user.admin?
で、管理ユーザーかどうかを判定することができる。
yokoyan:~/workspace/sample_app (updating-users) $ rails console --sandbox Running via Spring preloader in process 2524 Loading development environment in sandbox (Rails 5.0.0.1) Any modifications you make will be rolled back on exit >> user = User.first User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-06-13 03:57:28", updated_at: "2017-06-13 03:57:28", password_digest: "$2a$10$zGDH/dBloZT0Txbp62FHdO13Mt16cBWhQehUmwKcY4l...", remember_digest: nil, admin: false> >> user.admin? => false >> user.toggle!(:admin) (0.1ms) SAVEPOINT active_record_1 SQL (0.9ms) UPDATE "users" SET "updated_at" = ?, "admin" = ? WHERE "users"."id" = ? [["updated_at", 2017-06-14 21:59:19 UTC], ["admin", true], ["id", 1]] (0.1ms) RELEASE SAVEPOINT active_record_1 => true >> user.admin? => true
【seeds】最初のテストユーザーを管理者にする
最初のユーザに、admin:true
を追加する。
User.create!( name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true )
データベースをリセットする。
yokoyan:~/workspace/sample_app (updating-users) $ rails db:migrate:reset Dropped database 'db/development.sqlite3' Dropped database 'db/test.sqlite3' Created database 'db/development.sqlite3' Created database 'db/test.sqlite3' == 20170417215343 CreateUsers: migrating ====================================== -- create_table(:users) -> 0.0019s == 20170417215343 CreateUsers: migrated (0.0020s) ============================= == 20170423125906 AddIndexToUsersEmail: migrating ============================= -- add_index(:users, :email, {:unique=>true}) -> 0.0012s == 20170423125906 AddIndexToUsersEmail: migrated (0.0013s) ==================== == 20170424041610 AddPasswordDigestToUsers: migrating ========================= -- add_column(:users, :password_digest, :string) -> 0.0006s == 20170424041610 AddPasswordDigestToUsers: migrated (0.0012s) ================ == 20170514215307 AddRememberDigestToUsers: migrating ========================= -- add_column(:users, :remember_digest, :string) -> 0.0007s == 20170514215307 AddRememberDigestToUsers: migrated (0.0007s) ================ == 20170614215124 AddAdminToUsers: migrating ================================== -- add_column(:users, :admin, :boolean, {:default=>false}) -> 0.0011s == 20170614215124 AddAdminToUsers: migrated (0.0018s) =========================
サンプルデータを再作成する。
yokoyan:~/workspace/sample_app (updating-users) $ rails db:seed
【controller】Strong Parametersを使用する際の注意点
悪意のあるユーザーが、curlで以下のpatchリクエストが送信された場合、 17番のユーザーを管理者に変えてしまう。
patch /users/17?admin=1
編集しても良い安全な属性のみに絞るために、Strong Parametersを使用する。
許可されたパラメータの中に、admin
が含まれていないため、
任意のユーザーが自分自身に管理者権限を付与することを防ぐことができる。
def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end
演習1
Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL (/users/:id) に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は redになるはずです。
テストコードを修正。
@other_user
でログインする@other_user
が管理者ユーザではないこと@other_user
のパスワード、パスワード確認、管理ユーザ属性を更新する@other_user
の情報を再読み込みした結果、管理ユーザではないこと
test "should not allow the admin attribute to be edited via the web" do log_in_as(@other_user) assert_not @other_user.admin? patch user_path(@other_user), params: { user: { password: @other_user.password, password_confirmation: @other_user.password_confirmation, admin: true } } assert_not @other_user.reload.admin?
テストコードを実行。greenになった。
yokoyan:~/workspace/sample_app (updating-users) $ rails test Running via Spring preloader in process 2815 Started with run options --seed 56203 39/39: [================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.72821s 39 tests, 144 assertions, 0 failures, 0 errors, 0 skips
念の為、テスト用のデータベースに接続して、@other_user
であるarcherユーザーを検索する。
admin: false
になっていることを確認。
yokoyan:~/workspace/sample_app (updating-users) $ rails console -e test Running via Spring preloader in process 2798 Loading test environment (Rails 5.0.0.1) >> archer = User.find_by(email: 'duchess@example.gov') User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "duchess@example.gov"], ["LIMIT", 1]] => #<User id: 950961012, name: "Sterling Archer", email: "duchess@example.gov", created_at: "2017-06-15 21:49:10", updated_at: "2017-06-15 21:49:10", password_digest: "$2a$04$EZ0hNQFt3TuHfi68Y2D0FOIu0.LRcouoZMnp0mATvnG...", remember_digest: nil, admin: false>
10.4.2 destroyアクション
本章での学び
destroyアクションへのリンクを生成する。
【view】destroyアクションへのリンクを生成
管理ユーザでかつ、user
の値が現在ログイン中のユーザと異なる(自分自身ではない)場合に、
deleteリンクを表示する。
ブラウザはDELETEリクエストを送信できないため、RailsではJSで偽造している。
<li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> <% if current_user.admin? && !current_user?(user) %> | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %> <% end %> </li>
【controller】destroyアクションの作成
以下の仕様で実装する。
- beforeフィルターに、destroyアクションを追加する
- destroyアクションを作成する
- 対象ユーザを検索して、削除する
- 削除完了メッセージを表示する
- ユーザー一覧画面へリダイレクトする
def destroy User.find(params[:id]).destroy flash[:success] = "User deleted" redirect_to users_url end
【controller】管理者だけに限定するbeforeアクションの生成
悪意のあるユーザーがコマンドラインでDELETEリクエストを発行し、全ユーザの削除を試みることを防止するために、beforeアクションで制限をかける。
- 管理者であれば、destroyアクションを実行する
- 管理者でなければ、topページヘリダイレクトする
before_action :admin_user, only: :destroy ・・・省略・・・ # 管理者かどうか確認 def admin_user redirect_to(root_url) unless current_user.admin? end
演習1
管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。ユーザーを削除すると、Railsサーバーのログにはどのような情報が表示されるでしょうか?
4ページ目の先頭のユーザー(id:91)を削除する。
Railsサーバーのログは以下の通り。
tarted DELETE "/users/91" for 210.149.252.140 at 2017-06-17 00:51:08 +0000 Cannot render console from 210.149.252.140! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#destroy as HTML Parameters: {"authenticity_token"=>"cRvcWmDu4901k4Hf2u/trY0pQWaRZ8QDQ7I01ujNGCsBl5FWTfGw5u8TCsqYkzgmVo9Ao6L6/DCB0EArg0DDCw==", "id"=>"91"} User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 91], ["LIMIT", 1]] (0.1ms) begin transaction SQL (0.2ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 91]] (10.1ms) commit transaction Redirected to https://xxxxxxxxxxxxx/users Completed 302 Found in 16ms (ActiveRecord: 10.6ms) Started GET "/users" for 210.149.252.140 at 2017-06-17 00:51:09 +0000 Cannot render console from 210.149.252.140! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#index as HTML User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Rendering users/index.html.erb within layouts/application (0.1ms) SELECT COUNT(*) FROM "users" User Load (0.3ms) SELECT "users".* FROM "users" LIMIT ? OFFSET ? [["LIMIT", 30], ["OFFSET", 0]] Rendered collection of users/_user.html.erb [30 times] (7.5ms) Rendered users/index.html.erb within layouts/application (12.3ms) Rendered layouts/_rails_default.html.erb (29.8ms) Rendered layouts/_shim.html.erb (0.3ms) Rendered layouts/_header.html.erb (0.8ms) Rendered layouts/_footer.html.erb (0.7ms) Completed 200 OK in 51ms (Views: 48.8ms | ActiveRecord: 0.6ms)
【失敗談】findではなくfind_byを使うと、adminユーザ自身が消える
うっかりはまってしまったのでメモ。
destroyアクションを誤って、find_by
を使っていました。
このように記載すると、adminユーザ自身を削除してしまう動きになります。ご注意ください。
User.find_by(params[:id]).destroy
findとfind_byの違いは以下のページが参考になりました。
Railsのfindメソッドがすぐ分かる!find_byとの違いも即理解
findメソッド⇒引数に取るのはid(属性は取らない) find_byメソッド⇒引数に取るのは属性(idは取らない)
10.4.3 ユーザー削除のテスト
本章での学び
destroyアクションのテストを作成していきます。
【fixture】adminユーザの作成
admin: true
を追加する。
michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: true
【単体テスト】テストコードの作成
Usersコントローラの単体テストを行う。
テストコード1
- deleteリクエストを発行しても、ユーザ数が変化していないこと
- ログインしていないユーザーであれば、ログイン画面にリダイレクトされること
test "should redirect destroy when not logged in" do assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to login_url end
テストコード2
- deleteリクエストを発行しても、ユーザ数が変化していないこと
- ログイン済みであっても、管理者でなければホーム画面にリダイレクトされること
test "should redirect destroy when logged in as a non-admin" do log_in_as(@other_user) assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to root_url end
【統合テスト】削除リンクとユーザー削除
事前準備
- 管理者ユーザを用意
- 非管理者ユーザを用意
def setup @admin = users(:michael) @non_admin = users(:archer) end
テストケース1
- 管理者ユーザーでログインする
- GETリクエストを送信する(ユーザー一覧)
- ユーザー一覧のテンプレートが表示されること
- ‘div.pagination'が表示されること
- 1ページ目のユーザー(30ユーザー)を取得し、繰り返し処理を行う
- ユーザー名と、ユーザーページへのリンク属性が表示されること
- 管理者ユーザーでなければ、deleteリンクが表示されること
- ユーザーの件数が-1されていること
- DELETEリクエストを非管理者ユーザーに送信
test "index including pagination and delete links" do log_in_as(@admin) get users_path assert_template 'users/index' assert_select 'div.pagination', count: 2 User.paginate(page: 1).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' end end assert_difference 'User.count', -1 do delete user_path(@non_admin) end end
テストケース2
- 非管理者ユーザーでログインする
- GETリクエストを送信する(ユーザー一覧)
- 画面にaタグと、deleteのテキストが表示されないこと
test "index as non_admin" do log_in_as(@non_admin) get users_path assert_select 'a', text: 'delete', count: 0 end
演習1
試しにリスト 10.59にある管理者ユーザーのbeforeフィルターをコメントアウトしてみて、テストの結果が redに変わることを確認してみましょう。
destroyアクションに関する、beforeフィルターをコメントアウトする。
# before_action :admin_user, only: :destroy
テストがredになることを確認。
yokoyan:~/workspace/sample_app (updating-users) $ rails test Running via Spring preloader in process 3871 Started with run options --seed 48392 FAIL["test_should_redirect_destroy_when_logged_in_as_a_non_admin", UsersControllerTest, 0.4967454259749502] test_should_redirect_destroy_when_logged_in_as_a_non_admin#UsersControllerTest (0.50s) "User.count" didn't change by 0. Expected: 34 Actual: 33 test/controllers/users_controller_test.rb:66:in `block in <class:UsersControllerTest>' 42/42: [================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.90546s 42 tests, 178 assertions, 1 failures, 0 errors, 0 skips
動作確認ができたら、beforeフィルターを戻す。
10.5最後に
本章の学び
作成したアプリケーションを本番環境にデプロイする。
【heroku】本番環境へデプロイ
yokoyan:~/workspace/sample_app (master) $ git push heroku
【heroku】本番環境のDBのリセット
yokoyan:~/workspace/sample_app (master) $ heroku pg:reset DATABASE ▸ WARNING: Destructive action ▸ postgresql-infinite-44520 will lose all of its data ▸ ▸ To proceed, type enigmatic-everglades-70434 or re-run ▸ this command with --confirm enigmatic-everglades-70434 > enigmatic-everglades-70434 Resetting postgresql-infinite-44520... done
【heroku】本番環境のDBの再構築
yokoyan:~/workspace/sample_app (master) $ heroku run rails db:migrate Running rails db:migrate on ⬢ enigmatic-everglades-70434... up, run.6128 (Free) D, [2017-06-17T02:45:34.771345 #4] DEBUG -- : (40.3ms) CREATE TABLE "schema_migrations" ("version" character varying PRIMARY KEY) D, [2017-06-17T02:45:34.787029 #4] DEBUG -- : (12.0ms) CREATE TABLE "ar_internal_metadata" ("key" character varying PRIMARY KEY, "value" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL) D, [2017-06-17T02:45:34.788665 #4] DEBUG -- : (0.7ms) SELECT pg_try_advisory_lock(414146247089748600); D, [2017-06-17T02:45:34.796972 #4] DEBUG -- : ActiveRecord::SchemaMigration Load (0.7ms) SELECT "schema_migrations".* FROM "schema_migrations" I, [2017-06-17T02:45:34.806079 #4] INFO -- : Migrating to CreateUsers (20170417215343) D, [2017-06-17T02:45:34.807875 #4] DEBUG -- : (0.6ms) BEGIN == 20170417215343 CreateUsers: migrating ====================================== -- create_table(:users) D, [2017-06-17T02:45:34.826544 #4] DEBUG -- : (17.7ms) CREATE TABLE "users" ("id" serial primary key, "name" character varying, "email" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL) -> 0.0186s == 20170417215343 CreateUsers: migrated (0.0187s) ============================= D, [2017-06-17T02:45:34.832027 #4] DEBUG -- : SQL (0.8ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20170417215343"]] D, [2017-06-17T02:45:34.833905 #4] DEBUG -- : (1.6ms) COMMIT I, [2017-06-17T02:45:34.833987 #4] INFO -- : Migrating to AddIndexToUsersEmail (20170423125906) D, [2017-06-17T02:45:34.834803 #4] DEBUG -- : (0.5ms) BEGIN == 20170423125906 AddIndexToUsersEmail: migrating ============================= -- add_index(:users, :email, {:unique=>true}) D, [2017-06-17T02:45:34.854226 #4] DEBUG -- : (16.8ms) CREATE UNIQUE INDEX "index_users_on_email" ON "users" ("email") -> 0.0193s == 20170423125906 AddIndexToUsersEmail: migrated (0.0194s) ==================== D, [2017-06-17T02:45:34.857272 #4] DEBUG -- : SQL (2.2ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20170423125906"]] D, [2017-06-17T02:45:34.858838 #4] DEBUG -- : (1.4ms) COMMIT I, [2017-06-17T02:45:34.858915 #4] INFO -- : Migrating to AddPasswordDigestToUsers (20170424041610) D, [2017-06-17T02:45:34.859746 #4] DEBUG -- : (0.4ms) BEGIN == 20170424041610 AddPasswordDigestToUsers: migrating ========================= -- add_column(:users, :password_digest, :string) D, [2017-06-17T02:45:34.860892 #4] DEBUG -- : (0.8ms) ALTER TABLE "users" ADD "password_digest" character varying -> 0.0011s == 20170424041610 AddPasswordDigestToUsers: migrated (0.0011s) ================ D, [2017-06-17T02:45:34.862031 #4] DEBUG -- : SQL (0.6ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20170424041610"]] D, [2017-06-17T02:45:34.863391 #4] DEBUG -- : (1.2ms) COMMIT I, [2017-06-17T02:45:34.863462 #4] INFO -- : Migrating to AddRememberDigestToUsers (20170514215307) D, [2017-06-17T02:45:34.866125 #4] DEBUG -- : (2.4ms) BEGIN == 20170514215307 AddRememberDigestToUsers: migrating ========================= -- add_column(:users, :remember_digest, :string) D, [2017-06-17T02:45:34.867224 #4] DEBUG -- : (0.8ms) ALTER TABLE "users" ADD "remember_digest" character varying -> 0.0010s == 20170514215307 AddRememberDigestToUsers: migrated (0.0011s) ================ D, [2017-06-17T02:45:34.868336 #4] DEBUG -- : SQL (0.6ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20170514215307"]] D, [2017-06-17T02:45:34.877338 #4] DEBUG -- : (8.8ms) COMMIT I, [2017-06-17T02:45:34.877498 #4] INFO -- : Migrating to AddAdminToUsers (20170614215124) D, [2017-06-17T02:45:34.878487 #4] DEBUG -- : (0.5ms) BEGIN == 20170614215124 AddAdminToUsers: migrating ================================== -- add_column(:users, :admin, :boolean, {:default=>false}) D, [2017-06-17T02:45:34.889802 #4] DEBUG -- : (10.0ms) ALTER TABLE "users" ADD "admin" boolean DEFAULT 'f' -> 0.0111s == 20170614215124 AddAdminToUsers: migrated (0.0113s) ========================= D, [2017-06-17T02:45:34.891235 #4] DEBUG -- : SQL (0.7ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20170614215124"]] D, [2017-06-17T02:45:34.909464 #4] DEBUG -- : (17.9ms) COMMIT D, [2017-06-17T02:45:34.917199 #4] DEBUG -- : ActiveRecord::InternalMetadata Load (0.8ms) SELECT "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = $1 LIMIT $2 [["key", :environment], ["LIMIT", 1]] D, [2017-06-17T02:45:34.923379 #4] DEBUG -- : (0.6ms) BEGIN D, [2017-06-17T02:45:34.927184 #4] DEBUG -- : SQL (2.3ms) INSERT INTO "ar_internal_metadata" ("key", "value", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "key" [["key", "environment"], ["value", "production"], ["created_at", 2017-06-17 02:45:34 UTC], ["updated_at", 2017-06-17 02:45:34 UTC]] D, [2017-06-17T02:45:34.939513 #4] DEBUG -- : (12.0ms) COMMIT D, [2017-06-17T02:45:34.940590 #4] DEBUG -- : (0.8ms) SELECT pg_advisory_unlock(414146247089748600)
【heroku】本番DBにサンプルデータを登録する
heroku run rails db:seed
で、100件のユーザーを登録する。
yokoyan:~/workspace/sample_app (master) $ heroku run rails db:seed ・・・略・・・ D, [2017-06-17T02:48:10.991756 #4] DEBUG -- : SQL (0.6ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["name", "Adriana Purdy"], ["email", "example-99@railstutorial.org"], ["created_at", 2017-06-17 02:48:10 UTC], ["updated_at", 2017-06-17 02:48:10 UTC], ["password_digest", "$2a$10$hBTXrMXSJ3Klm64DWieZxuAeNMD8rocVbRLoBFvAka/E.c0bF4q9m"]] D, [2017-06-17T02:48:10.993204 #4] DEBUG -- : (1.2ms) COMMIT
【heroku】本番環境DBの再起動
yokoyan:~/workspace/sample_app (master) $ heroku restart Restarting dynos on ⬢ enigmatic-everglades-70434... done
おわりに
destroyアクションも完成し、無事にherokuへのデプロイ、 本番DBの再構築も行うことができました。
登録、更新、一覧表示、削除の基本的な機能を持つWEBアプリケーションを 自分の手でゼロから作ることができたのは感動です。
チュートリアルが終わったら、学んだことを応用して、 Railsでアプリケーションを自作したいと思います!