紙一重の積み重ね

アラフォーのエンジニアがなれる最高の自分を目指して、学んだことをこつこつ情報発信するブログです。

【10章】Ruby on Railsチュートリアル演習まとめ&解答例【10.4 ユーザーを削除する】

はじめに

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?メソッドも使えるようになる。

image

マイグレーションを実行して、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)を削除する。

image

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でアプリケーションを自作したいと思います!