学習記録 18日目
18日目の学習記録をまとめていきます。
- 学習計画
- 学習内容
- Ruby on Railsチュートリアル 9章
- Ruby on Rails 5 速習実践ガイド 3章
- 3-1-3 アプリケーションのひな形を作成する
- 3-1-5 ビュー層を効率良く書くためにSlimを使えるようにする
- 3-1-6 アプリケーションの見栄えを良くするためにBootstrapを導入する
- 3-1-7 Railsのエラーメッセージなどを日本語で出せるようにする
- 3-2-1 タスクモデルの属性を設計する
- 3-2-2 タスクモデルのひな形を作成する
- 3-2-3 マイグレーションでデータベースにテーブルを追加する
- 3-3 コントローラとビュー
- 3-3-1-1 一覧画面に新規登録リンクを追加する
- 3-3-1-2 モデルの翻訳情報を追加する
- 3-3-1-4 アクションへデータを送る「リクエストパラメータ」
- 3-3-1-5 新規登録画面のビューを実装する
- 3-3-1-6 登録アクションを実装する
- 3-3-1-8 Flashメッセージ
- 3-3-2-1 一覧表示アクションでタスクデータを取得する
- 3-3-2-2 一覧画面ですべてのタスクデータを表示する
- 3-3-3 詳細表示機能を実装する
- 3-3-3-1 指定されたタスクを詳細表示アクションで取得
- 3-3-3-2 詳細画面にタスクの属性情報を表示する
- 3-3-4 編集機能を実装する
- 3-3-4-1 パーシャルを使った新規登録画面と編集画面の共通化
- 3-3-5 削除機能を実装する
- 本日の総括
学習計画
- Railsチュートリアル 9章
- Ruby on Rails 5 速習実践ガイド 3章
学習内容
Ruby on Railsチュートリアル 9章
9.1 Remember me 機能
- 永続cookieを使用してブラウザを再起動した後でもログイン状態を保持できる機能を追加していく。
- トピックブランチを作成します。
$ git checkout -b advanced-login
9.1.1 記憶トークンと暗号化
- remember_digestカラムをusersテーブルに追加するマイグレーションを作成します。
$ rails g migration add_remember_digest_to_users remember_digest:string
- マイグレートします。
$ rails db:migrate
$ rails c > SecureRandom.urlsafe_base64 # => "y0Ih5bXtUwf50Z87qcNbbw"
- Userモデルのクラスメソッドにランダムなトークンを作成するnew_tokenメソッドを追記します。またremember_digestへデータベース登録するrememberメソッドを作成します。remember_digestにはnew_tokenで作成したランダムな文字列をUser.digestによりさらにハッシュ化させます。
# app/models/user.rb class User < ApplicationRecord attr_accessor :remember_token #... def User.new_token SecureRandom.urlsafe_base64 end def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) end end
演習
1. コンソールを開き、データベースにある最初のユーザーを変数userに代入してください。その後、そのuserオブジェクトからrememberメソッドがうまく動くかどうか確認してみましょう。また、remember_tokenとremember_digestの違いも確認してみてください。
$ rails c --sandbox > user = User.first > user.remember > user.remember_digest # => "$2a$12$8zaItKWLQkIIToSEo/VhwOK6UcngE15xP0S15x95ykcnZAi7AHCky" > user.remember_token # => "g8xySprIPmPf4qDrdLJJ5Q"
2. リスト 9.3では、明示的にUserクラスを呼び出すことで、新しいトークンやダイジェスト用のクラスメソッドを定義しました。実際、User.new_tokenやUser.digestを使って呼び出せるようになったので、おそらく最も明確なクラスメソッドの定義方法であると言えるでしょう。しかし実は、より「Ruby的に正しい」クラスメソッドの定義方法が2通りあります。1つはややわかりにくく、もう1つは非常に混乱するでしょう。テストスイートを実行して、ややわかりにくいリスト 9.4の実装でも、非常に混乱しやすいリスト 9.5の実装でも、いずれも正しく動くことを確認してみてください。ヒント: selfは、通常の文脈ではUser「モデル」、つまりユーザーオブジェクトのインスタンスを指しますが、リスト 9.4やリスト 9.5の文脈では、selfはUser「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります。
- 個人的にはselfが付いたらクラスメソッドと判断できselfの方が分かりやすいです。
# app/models/user.rb class User < ApplicationRecord #... def self.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end def self.new_token SecureRandom.urlsafe_base64 end #... end
9.1.2 ログイン状態の保持
- 個別のcookiesはvalueとexpiresからなるハッシュで出来ています。
cookies[:remember_token] = { value: remember_token, expires: 20.years.from_now.utc }
cookies.signed[:user_id] = user.id || - cookieの永続化(20年)をするにはparmanent(20年の期限を設定してくれるRailsのメソッド)メソッドを使用します。 >|ruby| cookies.parmanent.signed[:user_id] = user.id || - 渡されたトークンがremember_digestと一致したらtrueを返すメソッドを追記する。 >|ruby| # app/models/user.rb class User < ApplicationRecord #... def authenticated?(remember_token) BCrypt::Password.new(remember_digest).is_password?(remember_token) end #... end
- ここらへんの理解が曖昧だったのでrailsコンソールで動作確認してみます。
$ rails c > user = User.first > user.remember > user.remember_token # => "k3w1u-gPF8EfaPrGZswP2Q" > user.remember_digest # => "$2a$12$45ackForGev3kW.7yM45bezbceF3Q7Teq0U69IKiyoqB2serI3tFm" > BCrypt::Password.new(user.remember_digest).is_password?(user.remember_token) # => true # 本来の==であればfalseだがbcryptは==をオーバライドしており実際にはis_password?が使用されている。 > BCrypt::Password.new(user.remember_digest) == user.remember_token # => true
- ユーザーがログインした際にrememberメソッドを実行するようにする。まずはsessions_controller.rbに記載します。
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user&.authenticate(params[:session][:password]) log_in user remember user redirect_to user else flash.now[:danger] = "Invalid email/password combination" render 'new' end end def destroy log_out redirect_to root_url end end
- rememberメソッドはUserモデルでしか使用出来ないためヘルパーを書きます。永続署名付きセッションもここで作成する。署名付きのため攻撃への対策もしている。
# app/helpers/sessions_helper.rb module SessionsHelper #... def remember(user) user.remember cookies.permanent.signed[:user_id] = user_id cookies.permanent[:remember_token] = user.remember_token end end
- sessionsヘルパーのcurrent_userメソッドは一時セッションにしか対応していないため記載し直す。
# app/helpers/sessions_helper.rb module SessionsHelper #... def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) user = User.find_by(id: user_id) if user&.authenticated?(cookies[:remember_token]) log_in user @current_user = user end end end #... end
- これで永続ログインに対応しました。しかしログアウトしても永続セッションが残っているためログアウト処理が上手くいきません。よってテストを行っても失敗します。
$ rails t 23 tests, 64 assertions, 1 failures, 0 errors, 0 skips
演習
1. ブラウザのcookieを調べ、ログイン後のブラウザではremember_tokenと暗号化されたuser_idがあることを確認してみましょう。
- 作成されています。しっかり暗号化されています。
2. コンソールを開き、リスト 9.6のauthenticated?メソッドがうまく動くかどうか確かめてみましょう。
$ rails c > user = User.first > user.remember # => true > user.authenticated?(user.remember_token) # => true
9.1.3 ユーザーを忘れる
- ユーザがログアウトした際にremember_digest, 永続セッションを削除するようにします。
# app/models/user.rb class User < ApplicationRecord #... def forget update_attribute(:remember_digest, nil) end end
# app/helpers/sessions_helper.rb module SessionsHelper #... def log_out forget(current_user) session.delete(:user_id) @current_user = nil end #... def forget(user) user.forget cookies.delete(:user_id) cookies.delete(:remember_token) end end
- これでログアウト時に永続セッションが削除されログアウト処理が上手く作動する。
$ rails t 23 tests, 66 assertions, 0 failures, 0 errors, 0 skips
演習
1. ログアウトした後に、ブラウザの対応するcookiesが削除されていることを確認してみましょう。
- 削除されています。
9.1.4 2つの目立たないバグ
- 現状2つの小さなバグが残っている。
- 複数タブでログイン状態にしておきログアウト後別タブでログアウトしようとするとエラーが発生する。これはlog_outメソッド内のforget(current_user)のcurrent_userがnilになっているためである。対処法として、ユーザーのログアウト処理はログイン中の場合のみ実行させるようにする。
- 複数ブラウザでログイン状態にし片方のブラウザでログアウトした後もう片方のブラウザでアクセスするとエラーになる。これはデータベース上のremember_digestがnilになりauthenticated?が上手く作動していないためである。この問題を解決するにはremember_digestが存在しない時はfalseを返す処理をauthenticated?に追加する必要がある。
- まずはテストを書いていきます。
# test/integration/users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest #... test "login with valid information followed by logout" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } assert is_logged_in? assert_redirected_to @user follow_redirect! assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) delete logout_path assert_not is_logged_in? assert_redirected_to root_url # ここで別タブでのログアウトを行う delete logout_path follow_redirect! assert_select "a[href=?]", login_path assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0 end end
- もちろんテストは失敗します。
$ rails test:integration 6 tests, 36 assertions, 0 failures, 1 errors, 0 skips
- ログアウト処理をログイン中にしか行えないようにします。
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController #... def destroy log_out if logged_in? redirect_to root_url end end
$ rails test:integration 6 tests, 39 assertions, 0 failures, 0 errors, 0 skips
- 2番目の問題についてのテストを書いていきます。
# test/integration/users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest #... test "authenicated? shold return false for a user with nil digest" do assert_not @user.authenticated?('') end end
$ rails test:integration 7 tests, 39 assertions, 0 failures, 1 errors, 0 skips
- authenicated?に早期リターンを記載します。
# app/models/user.rb class User < ApplicationRecord #... def authenticated?(remember_token) return false if remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) end #... end
- これでテストが成功するようになりました。
$ rails t 24 tests, 67 assertions, 0 failures, 0 errors, 0 skips
演習
- 動作確認のため省略
9.2 [Remember me]チェックボックス
- まずビューにチェックボックスを表示するコードを追記します。
# app/views/sessions/new.html.erb <% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(url: login_path, scope: :session, local: true) do |f| %> #... <%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>remember me on this computer</span> <% end %> <%= f.submit "Log in", class: 'btn btn-primary'%> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>
- チェックボックスがチェックされた時永続セッションを作成します。
- チェックボックスはparamsで取得出来ます。上記フォームの場合ならparams[:session][:remember_me]で取得可能。チェックが付いてれば1, 付いてなければ0が格納されます。nilではないので注意。
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController #... def create user = User.find_by(email: params[:session][:email].downcase) if user&.authenticate(params[:session][:password]) log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_to user else flash.now[:danger] = "Invalid email/password combination" render 'new' end end #... end
演習
1. ブラウザでcookies情報を調べ、[remember me]をチェックしたときに意図した結果になっているかどうかを確認してみましょう。
- remember meをチェックした際のcookies情報です。意図した結果になっています。
2. コンソールを開き、三項演算子を使った実例を考えてみてください(コラム 9.2)。
$ rails c >puts 1.nil? ? "nilだね" : "nilじゃないね" nilじゃないね
9.3.1 [Remember me]ボックスをテストする
- まずはテストでログイン処理を行うヘルパーを記載する。アプリケーション内のlog_inメソッドと混合しないようlog_in_asという名前にする。
# test/test_helper.rb #... class ActiveSupport::TestCase #... def log_in_as(user) session[:user_id] = user.id end end
- integrationテストではsessionを直接取り扱うことが出来ないためSessionsリソースに対してpostを送信することで代用します。integrationテスト中の実行なのでActionDispatch::IntegrationTestクラス内に記載する。
# test/test_helper.rb #... class ActiveSupport::TestCase #... end class ActionDispatch::IntegrationTest def log_in_as(user, password: 'password', remember_me: '1') post login_path, params: { session: { email: user.email, password: password, remember_me: remember_me } } end end
- テストを書いていきます。
# test/integration/users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest #... test "login with remembering" do log_in_as(@user, remember_me: '1') assert_not_empty cookies[:remember_token] end test "login without remembering" do log_in_as(@user, remember_me: '1') assert_not_empty cookies[:remember_token] delete logout_path log_in_as(@user, remember_me: '0') assert_empty cookies[:remember_token] end end
- テストが成功することを確認します。
$ rails test:integration 9 tests, 46 assertions, 0 failures, 0 errors, 0 skips
演習
1. リスト 9.25の統合テストでは、仮想のremember_token属性にアクセスできないと説明しましたが、実は、assignsという特殊なテストメソッドを使うとアクセスできるようになります。コントローラで定義したインスタンス変数にテストの内部からアクセスするには、テスト内部でassignsメソッドを使います。このメソッドにはインスタンス変数に対応するシンボルを渡します。例えばcreateアクションで@userというインスタンス変数が定義されていれば、テスト内部ではassigns(:user)と書くことでインスタンス変数にアクセスできます。本チュートリアルのアプリケーションの場合、Sessionsコントローラのcreateアクションでは、userを(インスタンス変数ではない)通常のローカル変数として定義しましたが、これをインスタンス変数に変えてしまえば、cookiesにユーザーの記憶トークンが正しく含まれているかどうかをテストできるようになります。このアイデアに従ってリスト 9.27とリスト 9.28の不足分を埋め(ヒントとして?やFILL_INを目印に置いてあります)、[remember me]チェックボックスのテストを改良してみてください。17
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController #... def create @user = User.find_by(email: params[:session][:email].downcase) if @user&.authenticate(params[:session][:password]) log_in @user params[:session][:remember_me] == '1' ? remember(@user) : forget(@user) redirect_to @user else flash.now[:danger] = "Invalid email/password combination" render 'new' end end #... end
- テストを改良して成功することを確認します。
# test/integration/users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest #... test "login with remembering" do log_in_as(@user, remember_me: '1') assert_equal cookies[:remember_token], assigns(:user).remember_token end test "login without remembering" do log_in_as(@user, remember_me: '1') assert_equal cookies[:remember_token], assigns(:user).remember_token delete logout_path log_in_as(@user, remember_me: '0') assert_empty cookies[:remember_token] end end
$ rails t 26 tests, 71 assertions, 0 failures, 0 errors, 0 skips
9.3.2 [Remember me]をテストする
- current_userメソッド内のテストを全くしていないためわざとcurrent_userメソッド内でエラーを発生させます。
# app/helpers/sessions_helper.rb module SessionsHelper #... def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) raise user = User.find_by(id: user_id) if user&.authenticated?(cookies[:remember_token]) log_in user @current_user = user end end end #... end
- テストを実行すると成功してしまいます。
$ rails t 26 tests, 71 assertions, 0 failures, 0 errors, 0 skips
- SessionsHelperのテストを作成します。
# test/helpers/sessions_helper_test.rb require 'test_helper' class SessionsHelperTest < ActionView::TestCase def setup @user = users(:michael) remember(@user) end test "current_user returns right user when session is nil" do assert_equal @user, current_user assert is_logged_in? end test "current_user returns nil when remember digest is wrong" do @user.update_attribute(:remember_digest, User.digest(User.new_token)) assert_nil current_user end end
- まずsetupメソッド内のrememberメソッドで@userを永続セッションさせます。
- 1つ目のテストで@userとcurrent_userメソッドの戻り値が同じかを検証しています。これで先ほどraiseした箇所を調べています。is_logged_in?ヘルパーメソッドは一時セッションがあるかを確認しています。
- 2つ目のテストでremember_digestにセッションとは一致しないトークンを入れています。そしてcurrent_userメソッドが一致しないことを検知しnilを返すか検証しています。
- このテストはcurrent_userメソッド内のraiseにより失敗します。
$ rails t 28 tests, 71 assertions, 0 failures, 2 errors, 0 skips
- raiseを削除するとテストは成功します。
$ rails t 28 tests, 74 assertions, 0 failures, 0 errors, 0 skips
演習
1. リスト 9.33にあるauthenticated?の式を削除すると、リスト 9.31の2つ目のテストで失敗することを確かめてみましょう(このテストが正しい対象をテストしていることを確認してみましょう)。
- 該当部をコメントアウトしてテストしてみます。失敗しますね。
# app/helpers/sessions_helper.rb module SessionsHelper #... def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) user = User.find_by(id: user_id) if user# &.authenticated?(cookies[:remember_token]) log_in user @current_user = user end end end #... end
$ rails t 28 tests, 74 assertions, 1 failures, 0 errors, 0 skips
9.4 最後に
- リモートリポジトリにプッシュしてherokuにデプロイします。
$ git add -A $ git commit -m "Implement advanced login" $ git push origin advanced-login $ git checkout master $ git merge advanced-login $ git push origin master $ heroku maintenance:on $ git push heroku $ heroku run rails db:migrate $ heroku maintenance:off
- 以上で9章は終了です。
Ruby on Rails 5 速習実践ガイド 3章
3-1-3 アプリケーションのひな形を作成する
- scaffoldを使用せずにCRUDを備えたアプリケーションを作成します。
- データベースにはpostgresqlを使用します。データベースの指定は-dオプションで行います。
- railsアプリケーションを新規作成します。
$ rails new taskleaf -d postgresql
- 本アプリケーションもリモートリポジトリに上げていきます。
$ git init $ git add -A $ git commit -m "Initialize a Repository" $ git remote add origin https://github.com/***/***.git $ git push origin master
- データベースを作成します。
$ cd taskleaf/ $ rails db:create
- railsサーバの動作確認をします。
$ rails s
- 動いてますね。
3-1-5 ビュー層を効率良く書くためにSlimを使えるようにする
- RailsではデフォルトでERBを採用しているが本書ではSlimを使用していく。他にもテンプレートエンジンにはHalmがあるが個人的にSlimの文法の方が好みです。
- slim-rails, html2slim gemを追加します。Gemfileに追記してbundle installします。
$ bundle install
- 現在ビューファイルが3つ存在しているのでSlimへ変換しておきます。bundle execとはbundleでインストールされたgemをターミナルで使用するためのコマンドです。--deleteオプションで元ファイルを削除します。
$ bundle exec erb2slim app/views/layouts/ --delete
3-1-6 アプリケーションの見栄えを良くするためにBootstrapを導入する
- Gemfileに追記してbundle installします。
$ bundle install
- application.cssを削除してapplication.scssを作成してbootstrapをインポートします。
@import "bootstrap";
- コンテナを作成します。ヘッダーのサイト名についても記載しておきます。
doctype html html head title | Postleaf = csrf_meta_tags = csp_meta_tag = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' body .app-title.navbar.navbar-expand-md.navbar-light.bg-light .navbar-brand Taskleaf .container = yield
- divタグを記載せずにdivタグが書けるslimめっちゃ便利。
3-1-7 Railsのエラーメッセージなどを日本語で出せるようにする
- エラーメッセージを日本語化します。GitHub上に翻訳されたファイルがあるのでそれを利用します。ダウンロードします。本書ではwgetを使用しているが使用できなかったので同機能のcurlを使用してダウンロードする。
$ curl https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml > config/locales/ja.yml
- 設定ファイルを作成します。
$ cat > config/initializers/locale.rb Rails.application.config.i18n.default_locale = :ja
- リモートリポジトリへプッシュします。ブランチし忘れてmasterで作業してたのでstashに避難させてブランチを作成してからプッシュします。
$ git stash $ git checkout -b chapter-3-1 $ git stash apply $ git add -A $ git commit -m "Finish chapter 3-1" $ git push origin chapter-3-1
3-2-1 タスクモデルの属性を設計する
- トピックブランチをchapter-3-2へ移動します。
$ git checkout -b chapter-3-2
- Taskモデルにはname:string, description:textカラムを作成します。
3-2-2 タスクモデルのひな形を作成する
- generate modelコマンドでTaskモデルを作成する。
$ rails g model Task name:string description:text Running via Spring preloader in process 8842 invoke active_record create db/migrate/20201023063535_create_tasks.rb create app/models/task.rb invoke test_unit create test/models/task_test.rb create test/fixtures/tasks.yml
- マイグレーションファイルとモデル, テストが作成されました。
3-2-3 マイグレーションでデータベースにテーブルを追加する
- マイグレーションファイルを確認して問題なければマイグレートします。
$ rails db:migrate
- リモートリポジトリへプッシュします。
$ git add -A $ git commit -m "Finish chapter 3-2" $ git push origin chapter-3-2
3-3 コントローラとビュー
- トピックブランチを作成します。
$ git checkout -b chapter-3-3
- CRUDの機能を満足するアクションを備えたコントローラを作成します。
$ rails g controller tasks index show new edit
- ルーティングのtasksの記述をresourcesへ変更しRESTfulなルーティングを一括で行う。またルートディレクトリのアクションをtasks#indexにする。
# config/routes.rb Rails.application.routes.draw do root 'tasks#index' resources :tasks end
- 動作確認します。ちゃんと動作してますね。
3-3-1-1 一覧画面に新規登録リンクを追加する
- indexビューに新規登録リンクを追加します。
# app/views/tasks/index.html.slim h1 タスク一覧 = link_to '新規登録', new_task_path, class: 'btn btn-primary'
3-3-1-2 モデルの翻訳情報を追加する
- モデルを作成したので翻訳情報を追記します。
# config/locales/ja.yml --- ja: activerecord: errors: messages: record_invalid: 'バリデーションに失敗しました: %{errors}' restrict_dependent_destroy: has_one: "%{record}が存在しているので削除できません" has_many: "%{record}が存在しているので削除できません" models: task: タスク attributes: task: id: name: 名称 description: 詳しい説明 created_at: 登録日時 updated_at: 更新日時 #...
3-3-1-4 アクションへデータを送る「リクエストパラメータ」
- リクエストパラメータの送り方には2種類ある。
- POSTで送る。formから送信する。
- GETで送る。URLで?以降に送信したいパラメータを記述することで送信出来る。
- どちらともparamsで受け取ることが出来る。
3-3-1-5 新規登録画面のビューを実装する
- newアクションのビューを実装します。
# app/views/tasks/new.html.slim h1 タスクの新規登録 .nav.justify-content-end = link_to '一覧', tasks_path, class: 'nav-link' = form_with model: @task, local: true do |f| .form-group = f.label :name = f.text_field :name, class: 'form-control', id: 'task_name' .form-group = f.label :description = f.text_area :description, class: 'form-control', id: 'task_description' = f.submit nil, class: 'btn btn-primary'
- submitの文字がnilだが動作確認すると保存するになっている。
- これは翻訳情報のhelpers下に定義された文字を元に自動で挿入されている。
# config/locales/ja.yml #... helpers: select: prompt: 選択してください submit: create: 登録する submit: 保存する update: 更新する #...
3-3-1-6 登録アクションを実装する
# app/controllers/tasks_controller.rb class TasksController < ApplicationController #... def create task = Task.new(task_params) task.save! redirect_to tasks_url, notice: "タスク「#{task.name}」を登録しました。" end private def task_params params.requre(:task).permit(:name, :description) end end
3-3-1-8 Flashメッセージ
- リダイレクト後flashメッセージを表示します。以下のコードは
redirect_to tasks_url, notice: "タスク「#{task.name}」を登録しました。"
- 以下と同意である。
flash[:notice] = "タスク「#{task.name}」を登録しました。" redirect_to tasks_url
- flashメッセージを表示するコードを記載する。
# app/views/layouts/application.html.slim doctype html html head title | Postleaf = csrf_meta_tags = csp_meta_tag = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' body .app-title.navbar.navbar-expand-md.navbar-light.bg-light .navbar-brand Taskleaf .container - if flash.notice.present? .alert.alert-success= flash.notice = yield
- 動作確認します。
3-3-2-1 一覧表示アクションでタスクデータを取得する
- まずはindexビューで使用するインスタンス変数tasksを定義します。
# app/controllers/tasks_controller.rb class TasksController < ApplicationController def index @tasks = Task.all end #... end
3-3-2-2 一覧画面ですべてのタスクデータを表示する
- ビューでタスクを一覧表示する実装をします。
# app/views/tasks/index.html.slim h1 タスク一覧 = link_to '新規登録', new_task_path, class: 'btn btn-primary' .mb-3 table.table.table-hover thead.thead-default tr th= Task.human_attribute_name(:name) th= Task.human_attribute_name(:created_at) tbody - @tasks.each do |task| th= task.name th= task.created_at
3-3-3 詳細表示機能を実装する
- task.nameにリンクを付けて詳細表示画面に飛べるようにします。
# app/views/tasks/index.html.slim #... - @tasks.each do |task| th= link_to task.name, task th= task.created_at
3-3-3-1 指定されたタスクを詳細表示アクションで取得
- showアクションのインスタンス変数を設定します。
# app/controllers/tasks_controller.rb class TasksController < ApplicationController #... def show @task = Task.find(params[:id]) end #... end
3-3-3-2 詳細画面にタスクの属性情報を表示する
- showビューを実装します。
- simple_formatメソッドは改行(\n)をbrと解釈してくれる。hメソッドは<などを<へ変換してくれる。
# app/views/tasks/new.html.slim h1 タスクの詳細 .nav.justify-content-end = link_to '一覧', tasks_path, class: 'nav-link' table.table.table-hover tbody tr th= Task.human_attribute_name(:id) td= @task.id tr th= Task.human_attribute_name(:name) td= @task.name tr th= Task.human_attribute_name(:description) td= simple_format(h(@task.description), {}, sanitize: false, wrapper_tag: "div") tr th= Task.human_attribute_name(:created_at) td= @task.created_at tr th= Task.human_attribute_name(:updated_at) td= @task.updated_at
- 動作確認をします。一覧表示できました。
3-3-4 編集機能を実装する
- editアクション, ビューを実装していきます。
- まずはeditへのリンクをindexビューへ追記します。
# app/views/tasks/index.html.slim h1 タスク一覧 = link_to '新規登録', new_task_path, class: 'btn btn-primary' .mb-3 table.table.table-hover thead.thead-default tr th= Task.human_attribute_name(:name) th= Task.human_attribute_name(:created_at) th tbody - @tasks.each do |task| tr td= link_to task.name, task td= task.created_at td = link_to '編集', edit_task_path(task), class: 'btn btn-primary mr-3'
- editアクションにインスタンス変数を設定します。
- updateアクションにはupdate!メソッドでStrong Parameters経由でデータベースの更新を行っています。
# app/controllers/tasks_controller.rb class TasksController < ApplicationController #... def edit @task = Task.find(params[:id]) end def update task = Task.find(params[:id]) task.update!(task_params) redirect_to tasks_url, notice: "タスク「#{task.name}」を更新しました。" end #... end
- editビューを実装します。h1の内容以外newビューと同じ内容になっています。
# app/views/tasks/edit.html.slim h1 タスクの編集 .nav.justify-content-end = link_to '一覧', tasks_path, class: 'nav-link' = form_with model: @task, local: true do |f| .form-group = f.label :name = f.text_field :name, class: 'form-control', id: 'task_name' .form-group = f.label :description = f.text_area :description, rows: 5, class: 'form-control', id: 'task_description' = f.submit nil, class: 'btn btn-primary'
3-3-4-1 パーシャルを使った新規登録画面と編集画面の共通化
- パーシャルを使用してフォーム部を共通化します。
$ touch app/views/tasks/_form.html.slim
# app/views/tasks/_form.html.slim h1 タスクの編集 .nav.justify-content-end = link_to '一覧', tasks_path, class: 'nav-link' = form_with model: task, local: true do |f| .form-group = f.label :name = f.text_field :name, class: 'form-control', id: 'task_name' .form-group = f.label :description = f.text_area :description, rows: 5, class: 'form-control', id: 'task_description' = f.submit nil, class: 'btn btn-primary'
# app/views/tasks/new.html.slim h1 タスクの新規登録 = render partial: 'form', locals: { task: @task }
# app/views/tasks/edit.html.slim h1 タスクの編集 = render partial: 'form', locals: { task: @task }
- DRYになりました。localsでローカル変数を定義しています。
3-3-5 削除機能を実装する
- まずはindexビューに削除ボタンを実装します。taskに対してDELETEリクエストしています。
# app/views/tasks/index.html.slim #... tbody - @tasks.each do |task| tr td= link_to task.name, task td= task.created_at td = link_to '編集', edit_task_path(task), class: 'btn btn-primary mr-3' = link_to '削除', task, method: :delete, data: { confirm: "タスク「#{task.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'
- showビューにも同様のボタンを実装します。
# app/views/tasks/show.html.slim #... = link_to '編集', edit_task_path, class: 'btn btn-primary mr-3' = link_to '削除', @task, method: :delete, data: { confirm: "タスク「#{@task.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'
- Taskコントローラのdestroyアクションを実装します。
# app/controllers/tasks_controller.rb class TasksController < ApplicationController #... def destroy task = Task.find(params[:id]) task.destroy redirect_to rasks_url, notice: "タスク「#{task.name}」を削除しました。" end #... end
- 動作確認します。しっかり削除できています。
- 最後にリモートリポジトリにプッシュします。
$ git add -A $ git commit -m "Finish Chapter 3-3" $ git push origin chapter-3-3 $ git checkout master $ git merge chapter-3-3 $ git push origin master
- 以上で3章は終了です。
本日の総括
- 1周目の9章はついていくので精一杯だった記憶がありました。今回2周目ですが細かい部分の理解をする余裕が生まれていました。少しずつ成長出来てるのかな?明日も頑張りたい。