はじめに
最近、プロジェクト管理業務が業務の大半を占めており、
プログラムを書く機会がなかなかありません。
このままだとプログラムがまったく書けない人になってしまう危機感(迫り来る35歳定年説)と、
新しいことに挑戦したいという思いから、
Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版を学習中です。
業務で使うのはもっぱらJavaなのですが、Rails楽しいですね。
これまでEvernoteに記録していましたが、ソースコードの貼付けに限界を感じたため、
Qiitaで自分が学習した結果をアウトプットしていきます。
個人の解答例なので、誤りがあればご指摘ください。
動作環境
- 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でアプリケーションを自作したいと思います!