学習記録 20日目
20日目の学習記録をまとめていきます。
- 学習計画
- 学習内容
- Ruby on Railsチュートリアル 11章
- Ruby on Rails 5 速習実践ガイド 4章
- 4-2-2 NOT NULL制約
- 4-2-3 文字列カラムの長さを指定する
- 4-2-4 ユニークインデックスを作成する
- 4-3-3 必須かどうかの検証を追加する
- 4-3-4 コントローラとビューで検証エラーに対応する
- 4-3-5 文字列長の検証を追加する
- 4-3-6 オリジナルの検証コードを書く
- 4-5-2 Userモデルを作る
- 4-5-4-1 Userモデルにadminフラグを追加する
- 4-5-4-2 ユーザ管理のためのコントローラを実装する
- 4-5-6 ログインのフォームを表示する
- 4-5-7 ログインの実行
- 4-5-8 ログイン情報の取得を簡単にする
- 4-5-9 ログアウト機能を実装する
- 4-5-10 ログインしていなければタスク管理を利用できなくする
- 4-5-11-1 データベース上でUserとTaskを紐付ける
- 4-5-11-2 Railsの「関連」を定義する
- 4-5-11-3 ログインしているユーザーのTaskデータの登録
- 4-5-11-4 ログインしているユーザーのTaskデータだけを読み出す
- 4-5-12 管理機能を管理者ユーザだけに利用させるようにする
- 4-7 タスク一覧を作成日時の新しい順に表示する
- 4-8 scopeを活用する
- 4-9 フィルタを使い重複を避ける
- 4-10 詳しい説明に含まれるURLをリンクとして表示する
学習計画
- Railsチュートリアル 11章
- Ruby on Rails 5 速習実践ガイド 4章
学習内容
Ruby on Railsチュートリアル 11章
- ユーザの新規作成時にアカウントを有効化するステップを差し込みます。このステップを差し込む事でメールアドレスの本人確認を行う事が出来ます。
11.1 AccountActivationsリソース
- トピックブランチを作成します。
$ git checkout -b account-activation
11.1.1 AccountActivationsコントローラ
- まずはAccountActivationsコントローラを作成します。
$ rails g controller AccountActivations
- ルーティングを設定します。今回はメールにてアカウントを有効化するURLを送信しGETリクエストでパラメータを取得します。よってupdateアクションではなくeditアクションを使用します。
# cofig/routes.rb Rails.application.routes.draw do #... resources :account_activations, only: [:edit] end
演習
1. 現時点でテストスイートを実行すると green になることを確認してみましょう。
$ rails t 42 tests, 180 assertions, 0 failures, 0 errors, 0 skips
2. 表 11.2の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。
11.1.2 AccountActivationのデータモデル
- アカウントの有効化はメールのURLとデータベース上の文字列を比較する方法があるがデータベースの情報が漏れた場合多大な被害に繋がってしまう。rememberの時同様にトークンをハッシュ化してデータベースに保存する方法を行い対策していく。
- 必要になるカラムをusersテーブルに追加していきます。まずはマイグレーションを作成します。
$ rails g migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
- activatedカラムのデフォルト値はfalseにします。
# db/***_add_activation_to_users.rb class AddActivationToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :activation_digest, :string add_column :users, :activated, :boolean, default: false add_column :users, :activated_at, :datetime end end
- マイグレートします。
$ rails db:migrate
- 次にUserモデルが作成される直前にactivation_digestを作成するメソッドをコールバックを実装していきます。rememberトークン同様にactivationトークンにもゲッタ, セッタを作成しておきます。
# app/models/user.rb class User < ApplicationRecord attr_accessor :remember_token, :activation_token before_save :downcase_email before_create :create_activation_digest #... private def downcase_email self.email = email.downcase end def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end end
- seedやfixtureのユーザの有効化をしておきます。
- データベースをリセットします。
$ rails db:migrate:reset $ rails db:seed
演習
1. 本項での変更を加えた後、テストスイートが green のままになっていることを確認してみましょう。
$ rails t 42 tests, 180 assertions, 0 failures, 0 errors, 0 skips
2. コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると(Privateメソッドなので)NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。
$ rails c > user = User.new > user.create_activation_digest NoMethodError (private method `create_activation_digest' called for #<User:0x00007fcb7967c7e8>)
3. リスト 6.35で、メールアドレスの小文字化にはemail.downcase!という(代入せずに済む)メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。
# app/models/user.rb class User < ApplicationRecord #... private def downcase_email email.downcase! end #... end
$ rails t 42 tests, 180 assertions, 0 failures, 0 errors, 0 skips
11.2.1 送信メールのテンプレート
ActionMailerライブラリを使用しUserのメイラーを作成します。
$ rails g mailer UserMailer account_activation password_reset Running via Spring preloader in process 31021 create app/mailers/user_mailer.rb invoke erb create app/views/user_mailer create app/views/user_mailer/account_activation.text.erb create app/views/user_mailer/account_activation.html.erb create app/views/user_mailer/password_reset.text.erb create app/views/user_mailer/password_reset.html.erb invoke test_unit create test/mailers/user_mailer_test.rb create test/mailers/previews/user_mailer_preview.rb
- メイラーとメールのテンプレートファイル, テスト, プレビューが作成されました。
- メールの送信元アドレスを設定します。
# app/mailers/application_mailer.rb class ApplicationMailer < ActionMailer::Base default from: 'noreply@example.com' layout 'mailer' end
- user_mailerメイラーのaccount_activationアクションの実装をしていきます。ビューでuserの情報を使用したいのでインスタンス変数@userを作成します。また送信先やメールの件名を設定します。
# app/mailers/user_mailer.rb class UserMailer < ApplicationMailer def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end #... end
- メールで送信するビューを作成します。URL部はedit_account_activation_url(@user.activated_token, email: @user.email)と記述します。
edit_account_activation_url(@user.activated_token, email: @user.email) # => /account_activations/__token__/edit?email=foo%40example.com
演習
1. コンソールを開き、CGIモジュールのescapeメソッド(リスト 11.15)でメールアドレスの文字列をエスケープできることを確認してみましょう。このメソッドで"Don't panic!"をエスケープすると、どんな結果になりますか?
$ rails c > CGI.escape("foo@example.com") # => "foo%40example.com" > CGI.escape("Don't panic!") # => "Don%27t+panic%21"
11.2.2 送信メールのプレビュー
- ローカル環境のメール設定をします。
# config/environments/development.rb Rails.application.configure do #... config.action_mailer.raise_delivery_errors = false host = 'localhost:3000' config.action_mailer.default_url_options = { host: host, protocol: 'http' } config.action_mailer.perform_caching = false #... end
- メイラープレビューの設定を行います。User.firstでプレビューを行います。
# test/mailers/previews/user_mailer_preview.rb class UserMailerPreview < ActionMailer::Preview def account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) end def password_reset UserMailer.password_reset end end
- プレビューで動作確認します。ちゃんと動いてますね。ローカル環境の設定を変えたのでRailsサーバを再起動しないと上手く動きません。
11.2.3 送信メールのテスト
- メイラーのテストを作成します。
# test/mailers/user_mailer_test.rb require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end end
- テストは失敗します。
$ rails test:mailers ERROR["test_account_activation", #<Minitest::Reporters::Suite:0x00007f88d27bcca0 @name="UserMailerTest">, 0.1705979999999272] test_account_activation#UserMailerTest (0.17s) ActionView::Template::Error: ActionView::Template::Error: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true app/views/user_mailer/account_activation.html.erb:9 app/mailers/user_mailer.rb:5:in `account_activation' test/mailers/user_mailer_test.rb:8:in `block in <class:UserMailerTest>' 1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
- default_url_options[:host]を設定してとエラーメッセージが出ているので設定します。
# config/environments/test.rb Rails.application.configure do #... config.action_mailer.default_url_options = { host: 'example.com' } #... end
- テストが成功することを確認します。
$ rails test:mailers 1 tests, 9 assertions, 0 failures, 0 errors, 0 skips
演習
1. この時点で、テストスイートが green になっていることを確認してみましょう。
$ rails t 43 tests, 189 assertions, 0 failures, 0 errors, 0 skips
2. リスト 11.20で使ったCGI.escapeの部分を削除すると、テストが red に変わることを確認してみましょう。
# test/mailers/user_mailer_test.rb require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do #... assert_match user.email, mail.body.encoded end end
$ rails test:mailers 1 tests, 9 assertions, 1 failures, 0 errors, 0 skips
11.2.4 ユーザーのcreateアクションを更新
- Userコントローラcreateアクションにてメイラーアクションを実行します。deliver_nowメソッドでメールの送信を行います。
# app/controllers/users_controller.rb class UsersController < ApplicationController #... def create @user = User.new(user_params) if @user.save UserMailer.account_activation(@user).deliver_now flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end end #... end
- リダイレクト先をroot_urlへ変更したため一部テストが失敗するようになる。一時的にコメントアウトしてテストが成功するようにする。
$ rails t FAIL["test_valid_signup_information", #<Minitest::Reporters::Suite:0x00007fbc270d0df0 @name="UsersSignupTest">, 2.888349000000062] test_valid_signup_information#UsersSignupTest (2.89s) expecting <"users/show"> but rendering with <["user_mailer/account_activation", "layouts/mailer", "static_pages/home", "layouts/_rails_default", "layouts/_shim", "layouts/_header", "layouts/_footer", "layouts/application"]> test/integration/users_signup_test.rb:26:in `block in <class:UsersSignupTest>' 43 tests, 187 assertions, 1 failures, 0 errors, 0 skips
# test/integration/users_signup_test.rb require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest #... test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar" }} end follow_redirect! # assert_template 'users/show' # assert_not flash.empty? # assert is_logged_in? end end
$ rails t 43 tests, 186 assertions, 0 failures, 0 errors, 0 skips
演習
1. 新しいユーザーを登録したとき、リダイレクト先が適切なURLに変わったことを確認してみましょう。その後、Railsサーバーのログから送信メールの内容を確認してみてください。有効化トークンの値はどうなっていますか?
- root_urlにリダイレクトされサーバログに送信内容が記載されています。
2. コンソールを開き、データベース上にユーザーが作成されたことを確認してみましょう。また、このユーザーはデータベース上にはいますが、有効化のステータスがfalseのままになっていることを確認してください。
$ rails c > user = User.last > user # => #<User id: 101, name: "hogehoge", email: "foo@bar.com", created_at: "2020-10-26 02:06:55", updated_at: "2020-10-26 02:06:55", password_digest: [FILTERED], remember_digest: nil, admin: false, activation_digest: "$2a$12$1gMU2ovlg9EEmzZRcOJ0CePlvkkYLMLWLG8MYIGl1N4...", activated: false, activated_at: nil>
authenticated?メソッドの抽象化
- 現在のauthenicated?メソッドはremember_digestに特化しているためそのままではactivation_digestには使用出来ない。Rubyのsendメソッドを使用することでauthenicated?メソッドを抽象化しactivation_digestにも使用出来るようにする。
# app/models/user.br class User < ApplicationRecord #... def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end #... end
- テストをしてみると失敗します。
$ rails t ERROR["test_authenicated?_shold_return_false_for_a_user_with_nil_digest", #<Minitest::Reporters::Suite:0x00007fbc23323520 @name="UsersLoginTest">, 2.3825440000000526] test_authenicated?_shold_return_false_for_a_user_with_nil_digest#UsersLoginTest (2.38s) ArgumentError: ArgumentError: wrong number of arguments (given 1, expected 2) app/models/user.rb:26:in `authenticated?' test/integration/users_login_test.rb:54:in `block in <class:UsersLoginTest>' ERROR["test_current_user_returns_nil_when_remember_digest_is_wrong", #<Minitest::Reporters::Suite:0x00007fbc22997e48 @name="SessionsHelperTest">, 2.5347179999998843] test_current_user_returns_nil_when_remember_digest_is_wrong#SessionsHelperTest (2.53s) ArgumentError: ArgumentError: wrong number of arguments (given 1, expected 2) app/models/user.rb:26:in `authenticated?' app/helpers/sessions_helper.rb:11:in `current_user' test/helpers/sessions_helper_test.rb:17:in `block in <class:SessionsHelperTest>' ERROR["test_current_user_returns_right_user_when_session_is_nil", #<Minitest::Reporters::Suite:0x00007fbc229491d0 @name="SessionsHelperTest">, 2.550060999999914] test_current_user_returns_right_user_when_session_is_nil#SessionsHelperTest (2.55s) ArgumentError: ArgumentError: wrong number of arguments (given 1, expected 2) app/models/user.rb:26:in `authenticated?' app/helpers/sessions_helper.rb:11:in `current_user' test/helpers/sessions_helper_test.rb:11:in `block in <class:SessionsHelperTest>' 43 tests, 182 assertions, 0 failures, 3 errors, 0 skips
- authenticated?メソッドを変更し引数にattributeを追加したために起きたエラーです。authenticated?メソッドを使用している箇所を修正します。
# 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?(:remember, cookies[:remember_token]) log_in user @current_user = user end end end #... end
# 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?(:remember, '') end #... end
$ rails t 43 tests, 186 assertions, 0 failures, 0 errors, 0 skips
- 無事テストが通過しました。
演習
1. コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?
- remember_token, remember_digestの作成はrememberメソッド実行時に行われる。rememberメソッドはログイン時のチェックボックスがチェックされた際に実行されるためUserの新規作成だけだとnilになる。
$ rails c > user = User.create(name: "Foo Bar", email: "foo@bar.org", password: "foobar", password_confirmation: "foobar") > user.activation_token # => "8fMEdsgTaYRzJKByl4LVRg" > user.activation_digest # => "$2a$12$2.CmyJwWcUIRtGBV4/rZOuwbuHSaMyWOX0haHdvsfESN3XvZGoxWO" > user.remember_token # => nil > user.remember_digest # => nil
2. リスト 11.26で抽象化したauthenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。
- やっぱりnilだと通らないですね。rememberメソッドを実行してからだと通るようになります。
> user.authenticated?(:activation, user.activation_token) # => true > user.authenticated?(:remember, user.remember_token) > user.remember # => true > user.remember_token # => "SFEzXdHBv3GukTxR8QwMzQ" > user.remember_digest # => "$2a$12$a3F9MiPMe2cdLIBJCXUUveRf8MnWQc6wQh69tjYajXRBgi0ulm3Oe" > user.authenticated?(:remember, user.remember_token) # => true
11.3.2 editアクションで有効化
- AccountActivationsコントローラのeditアクションを書いていきます。
# app/controllers/account_activations_controller.rb class AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now) log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end
- 動作確認をしてみます。ちゃんと動作しました。
- 有効化されてないユーザがログインしようとしてもログイン出来ないようにします。
# 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]) if @user.activated? log_in @user params[:session][:remember_me] == '1' ? remember(@user) : forget(@user) redirect_back_or @user else message = "Account not acticated." message += "Check your email for the activation link." flash[:warning] = message redirect_to root_url end else flash.now[:danger] = "Invalid email/password combination" render 'new' end end #... end
- 動作確認します。
演習
1. コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?
# URL http://localhost:3000/account_activations/t8k1wzzpYqxopAPlXFmUlw/edit?email=hogehoge%40hoge.com # 有効化トークン t8k1wzzpYqxopAPlXFmUlw
2. 先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。また、有効化ステータスがtrueになっていることをコンソールから確認してみてください。
$ rails c > User.last.activated # => true
11.3.3 有効化のテストとリファクタリング
# test/integration/users_signuo_test.rb require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest def setup ActionMailer::Base.deliveries.clear end #... test "valid signup information with account activation" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar" }} end assert_equal 1, ActionMailer::Base.deliveries.size user = assigns(:user) assert_not user.activated? log_in_as(user) assert_not is_logged_in? get edit_account_activation_path("invalid token", email: user.email) assert_not is_logged_in? get edit_account_activation_path(user.activation_token, email: "wrong") assert_not is_logged_in? get edit_account_activation_path(user.activation_token, email: user.email) assert user.reload.activated? follow_redirect! assert_template 'users/show' assert is_logged_in? end end
- テストが成功することを確認します。
$ rails t 43 tests, 194 assertions, 0 failures, 0 errors, 0 skips
- テストを作成したのでリファクタリングを行っていきます。
# app/models/user.rb class User < ApplicationRecord #... def send_activation_email UserMailer.account_activation(self).deliver_now end def activate update_attribute(:activated, true) update_attribute(:activated_at, Time.zone.now) end #... end
# app/controllers/user_controller.rb class UsersController < ApplicationController #... def create @user = User.new(user_params) if @user.save @user.send_activation_email flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end end #... end
# app/controllers/account_activations_controller.rb class AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.activate log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end
- リファクタリング後もテストが成功することを確認します。
$ rails t 43 tests, 194 assertions, 0 failures, 0 errors, 0 skips
演習
1. リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう。これでデータベースへの問い合わせが1回で済むようになります(注意!update_columnsは、モデルのコールバックやバリデーションが実行されない点がupdate_attributeと異なります)。また、変更後にテストを実行し、 green になることも確認してください。
# app/models/user.rb class User < ApplicationRecord #... def activate update_columns(activated: true, activated_at: Time.zone.now) end #... end
$ rails t 43 tests, 194 assertions, 0 failures, 0 errors, 0 skips
2. 現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト 11.40のテンプレートを使って、この動作を変更してみましょう9 。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。
# app/controllers/users_controller.rb class UsersController < ApplicationController #... def index @users = User.where(activated: true).paginate(page: params[:page]) end #... def show @user = User.find(params[:id]) redirect_to root_url and return unless @user.activated? end #... end
3. ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。
# test/integration/users_index_test.rb require 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest #... test "index shouldn't include not activated account" do log_in_as(@admin) post users_path, params: { user: { name: "test account", email: "test@test.com", password: "foobar", password_confirmation: "foobar" } } non_active_user = User.last last_page_num = ( User.count / 30 ).ceil get users_path(page: last_page_num) assert_select 'a[href=?]', user_path(non_active_user), count: 0 end test "should redirect with not activated account show page" do post users_path, params: { user: { name: "Foo Bar", email: "foo@bar.com", password: "foobar", password_confirmation: "foobar" } } non_active_user = User.last get user_path(non_active_user) assert_redirected_to root_url end end
$ rails t 45 tests, 196 assertions, 0 failures, 0 errors, 0 skips
11.4 本番環境でのメール送信
- herokuのSendGridアドオンを使用して本番環境でメール送信出来るようにします。設定ファイルに追記を行います。
- 本番環境で確認するためにデプロイを行います。
$ rails t $ git add -A $ git commit -m "Add account activation" $ git push origin account-activation $ git checkout master $ git merge account-activation $ git push origin master $ git push heroku $ heroku run rails db:migrate
- herokuにSendGridアドオンを追加します。
$ heroku addons:create sendgrid:starter
- 動作確認しましたが1周目でも出たSendGrid凍結問題が発生。メールの受信確認はせずにherokuのlogsコマンドからURLを見つけ動作確認します。(herokuのアプリを作り直せば凍結は一時的に治るがまたすぐ凍結されるため今回は諦めておきます)
$ heroku logs 2020-10-26T04:38:38.180809+00:00 app[web.1]: [0adb71bd-ab75-4e1f-b785-fb1a41b61343] Net::SMTPAuthenticationError (535 Authentication failed: account disabled
- 有効化まで確認しました。
- 以上で11章は終了です。
Ruby on Rails 5 速習実践ガイド 4章
4-2-2 NOT NULL制約
- カラムの値にNULLを格納する必要が無い場合はNOT NULL制約を付けることでNULLを入れないよう制限出来る。
- Taskleafアプリのnameカラムの中身がNULLの場合リンクが表示されず不便のためNOT NULL制約を付けていく。
$ git checkout -b chapter-4-2 $ rails g migration ChangeTasksNameNotNull
# db/migrate/***_chage_tasks_name_not_null.rb class ChangeTasksNameNotNull < ActiveRecord::Migration[6.0] def change chage_column_null :tasks, :name, false end end
$ rails db:migrate
- 試しにNULLを入れてみます。
$ rails c > Task.new(name: nil).save ActiveRecord::NotNullViolation (PG::NotNullViolation: ERROR: null value in column "name" of relation "tasks" violates not-null constraint)
4-2-3 文字列カラムの長さを指定する
4-2-4 ユニークインデックスを作成する
- ユニークインデックスを作成することで重複を防ぐ事が出来る。
4-3-3 必須かどうかの検証を追加する
- Taskモデルにバリデーションを追加します。persisted?メソッドはデータベース上に保存されたかを確認するメソッドです。
# app/models/task.rb class Task < ApplicationRecord validates :name, presence: true end
$ rails c > task = Task.new > task.save # => false > task.errors.full_messages # => ["Nameを入力してください"] > task.persisted? # => false > task = Task.new(name: "テストタスク") > task.save # => true > task.persisted? # => true
4-3-4 コントローラとビューで検証エラーに対応する
- createアクションでは現状save!メソッドを使用している。saveメソッドへ変更しタスクが検証エラー担った場合newビューをレンダリングするよう変更する。またnewビューはレンダリングするだけでアクションは実行されてないのでtaskをインスタンス変数化してビューで使用出来るようにする必要がある。
# app/controllers/tasks_controller.rb class TasksController < ApplicationController #... def create @task = Task.new(task_params) if @task.save redirect_to tasks_url, notice: "タスク「#{@task.name}」を登録しました。" else render :new end end #... end
- formパーシャルにエラーメッセージを表示する処理を実装します。
# app/views/tasks/_form.html.slim #... - if task.errors.present? ul#error_explanation - task.errors.full_messages.each do |message| li= message = form_with model: task, local: true do |f| #...
4-3-5 文字列長の検証を追加する
- nameの文字列を30文字以内に制限する。
# app/models/task.rb class Task < ApplicationRecord validates :name, presence: true, length: { maximum: 30 } end
4-3-6 オリジナルの検証コードを書く
- before_actionのようにvalidateはメソッドを実行してチェック出来る。以下はnameカラムにカンマの含む文字列には制限を書ける例である。
# app/models/task.rb class Task < ApplicationRecord validates :name, presence: true, length: { maximum: 30 } validate :validate_name_not_including_comma def validate_name_not_including_comma errors.add(:name, 'にカンマを含めることはできません') if name&.include?(',') end end
$ rails c > task = Task.new > task.name = "タスク,テスト" > task.save # => false > task.errors.full_messages # => ["Nameにカンマを含めることはできません"]
4-5-2 Userモデルを作る
- bcrypt gemを使用してハッシュ化したpasswordをデータベース上に保存します。まずはname, email, password_digestを持ったUserモデルを作成します。
$ rails g model User name:string email:string password_digest:string
- マイグレーションを編集してNOT NULL制約を追加します。またemailは一意性を保つためにインデックスを作成しunique: trueする。
# db/migrate/***_create_users.rb class CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :name, null: false t.string :email, null: false t.string :password_digest, null: false t.timestamps t.index :email, unique: true end end end
- マイグレートします。
$ rails db:migrate
- bcrypt gemを追加します。
- has_secure_passwordが使用出来るようになりました。これでUserモデルはpasswordとpassword_confirmationの2つの仮想属性を持てるようになりました。
# app/models/user.rb class User < ApplicationRecord has_secure_password end
- railsコンソールで動作確認してみます。password_digestにはハッシュ化された文字列が格納されています。
$ rails c --sandbox > user = User.new(name: "ユーザー", email: "sample@example.com", password: "foobar", password_confirmation: "foobar") > user.password_digest # => "$2a$12$31CEhg2yb7qMszwPPUOiQ.sYfgfiCShN0EEBy2pj3c4gHzUj15aU."
4-5-4-1 Userモデルにadminフラグを追加する
- ユーザが管理者かどうかを表すフラグを追加します。
$ rails g migration add_admin_to_users
# db/migrate/***_add_admin_to_users.rb class AddAdminToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :admin, :boolean, default: false, null: false end end
$ rails db:migrate
4-5-4-2 ユーザ管理のためのコントローラを実装する
- Admin::UserControllerコントローラを作成します。これはAdminモジュールの名前空間にUserControllerクラスを作成するという意味になります。Railsではモジュール階層がディレクトリ階層に対応しておりコマンドを実行するとapp/controllers/admin/users_controller.rbが作成される。new, edit, show, indexビューを作成します。
$ rails g controller Admin::Users new edit show index
- ルーティングを書いていきます。
# config/routes.rb Rails.application.routes.draw do namespace :admin do resources :users end root 'tasks#index' resources :tasks end
- new, createアクション, newビューを作成します。
# app/controllers/admin/users_controller.rb class Admin::UsersController < ApplicationController def new @user = User.new end def create @user = User.new(user_params) if @user.save redirect_to admin_user_url(@user), notice: "ユーザー「#{@user.name}」を登録しました。" else render :new end end #... private def user_params params.require(:user).permit(:name, :email, :admin, :password, :password_confirmation) end end
# app/views/admin/users/new.html.slim h1 ユーザー登録 = form_with model: [:admin, @user], local: true do |f| .form-group = f.label :name, '名前' = f.text_field :name, class: 'form-control' .form-group = f.label :email, 'メールアドレス' = f.email_field :email, class: 'form-control' .form-check = f.label :admin, class: 'form-check-label' do = f.check_box :admin, class: 'form-check-input' | 管理者権限 .form-group = f.label :password, 'パスワード' = f.password_field :password, class: 'form-control' .form-group = f.label :password_confirmation, 'パスワード(確認用)' = f.password_field :password_confirmation, class: 'form-control' = f.submit '登録する', class: 'btn btn-primary'
- Userクラスの検証を追記します。
# app/models/user.rb class User < ApplicationRecord has_secure_password validates :name, presence: true validates :email, presence: true, uniqueness: true end
- 動作確認をします。
- showアクション, ビューを作成します。
# app/controllers/admin/users_controller.rb class Admin::UsersController < ApplicationController #... def show @user = User.find(params[:id]) end #... end
# app/views/admin/users/show.html.slim h1 ユーザーの詳細 .nav.justify-content-end = link_to '一覧', admin_users_path, class: 'nav-link' table.table.table-hover tbody tr th= User.human_attribute_name(:id) td= @user.id tr th= User.human_attribute_name(:name) td= @user.name tr th= User.human_attribute_name(:email) td= @user.email tr th= User.human_attribute_name(:admin) td= @user.admin? ? 'あり' : 'なし' tr th= User.human_attribute_name(:created_at) td= @user.created_at tr th= User.human_attribute_name(:updated_at) td= @user.updated_at = link_to '編集', edit_admin_user_path, class: 'btn btn-primary mr-3' = link_to '削除', [:admin, @user], method: :delete, data: { confirm: "ユーザー「#{@user.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'
- 動作確認します。ユーザの作成が出来ました。
- 次にユーザーの一覧ページを作っていきます。
# app/controllers/admin/users_controller.rb class Admin::UsersController < ApplicationController #... def index @users = User.all end #... end
# app/views/admin/users/index.html.slim h1 ユーザー一覧 = link_to '新規登録', new_admin_user_path, class: 'btn btn-primary' .mb-3 table.table.table-hover thead.thead-default tr th= User.human_attribute_name(:name) th= User.human_attribute_name(:email) th= User.human_attribute_name(:admin) th= User.human_attribute_name(:created_at) th= User.human_attribute_name(:updated_at) th tbody - @users.each do |user| tr td= link_to user.name, [:admin, user] td= user.email td= user.admin? ? 'あり' : 'なし' td= user.created_at td= user.updated_at td = link_to '編集', edit_admin_user_path(user), class: 'btn btn-primary mr-3' = link_to '削除', [:admin, user], method: :delete, data: { confirm: "ユーザー「#{user.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'
- 動作確認をします。
- 先ほどnewで作成したform部分をパーシャルします。エラーメッセージが表示されるようにしておきます。
# app/views/admin/users/_form.html.slim - if user.errors.present? ul#error_explanation - user.errors.full_messages.each do |message| li= message = form_with model: [:admin, user], local: true do |f| .form-group = f.label :name, '名前' = f.text_field :name, class: 'form-control' .form-group = f.label :email, 'メールアドレス' = f.email_field :email, class: 'form-control' .form-check = f.label :admin, class: 'form-check-label' do = f.check_box :admin, class: 'form-check-input' | 管理者権限 .form-group = f.label :password, 'パスワード' = f.password_field :password, class: 'form-control' .form-group = f.label :password_confirmation, 'パスワード(確認用)' = f.password_field :password_confirmation, class: 'form-control' = f.submit '登録する', class: 'btn btn-primary'
# app/controllers/admin/users_controller.rb h1 ユーザー登録 .nav.justify-content-end = link_to '一覧', admin_users_path, class: 'nav-link' = render partial: 'form', locals: { user: @user }
- editアクション, ビューを作成します。
# app/controllers/admin/users_controller.rb class Admin::UsersController < ApplicationController #... def edit @user = User.find(params[:id]) end #... end
# app/views/admin/users/edit.html.slim h1 ユーザー編集 .nav.justify-content-end = link_to '一覧', admin_users_path, class: 'nav-link' = render partial: 'form', locals: { user: @user }
- 動作確認をします。
- updateアクションを作成します。
# app/controllers/admin/users_controller.rb class Admin::UsersController < ApplicationController #... def update @user = User.find(params[:id]) if @user.update(user_params) redirect_to admin_user_url(@user), notice: "ユーザー「#{@user.name}」を更新しました。" else render :edit end end #... end
- 動作確認をします。
- destroyアクションを作成します。
# app/controllers/admin/users_controller.rb class Admin::UsersController < ApplicationController #... def destroy @user = User.find(params[:id]) @user.destroy redirect_to admin_users_url, notice: "ユーザー「#{@user.name}」を削除しました。" end #... end
- 動作確認します。
- 最後に翻訳情報を追記します。
- 以上でCRUD機能が完成しました。
4-5-6 ログインのフォームを表示する
- 次にログイン機能を追加していきます。まずはSessionsControllerを作成します。
$ rails g controller Sessions new
- ルーティングにて名前付きルートを設定します。
# config/routes.rb Rails.application.routes.draw do get '/login', to: 'sessions/new' #... end
- newビューにログイン画面を実装します。
# app/views/sessions/new.html.slim h1 ログイン = form_with scope: :session, local: true do |f| .form-group = f.label :email, 'メールアドレス' = f.text_field :email, class: 'form-control', id: 'session_email' .form-group = f.label :password, 'パスワード' = f.password_field :password, class: 'form-control', id: 'session_password' = f.submit 'ログインする', class: 'btn btn-primary'
- ビューの動作確認をします。
4-5-7 ログインの実行
- ログインの実装をします。まずはルーティングにて名前付きルートを設定します。
# config/routes.rb Rails.application.routes.draw do get '/login', to: 'sessions/new' post '/login', to: 'sessions#create' #... end
- createアクションを実装します。
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: session_params[:email]) if user&.authenticate(session_params[:password]) session[:user_id] = user.id redirect_to root_url, notice: 'ログインしました。' else render :new end end private def session_params params.require(:session).permit(:email, :password) end end
- ログイン機能の動作確認をします。
4-5-8 ログイン情報の取得を簡単にする
- ログインしたユーザーの情報をどのコントローラ, ビューからでも取得出来るようにしておく。helper_methodで指定したメソッドはビューから使用出来る。
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base helper_method :current_user private def current_user @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] end end
4-5-9 ログアウト機能を実装する
- まずはログアウトのルーティングを作成します。
# config/routes.rb Rails.application.routes.draw do #... delete '/logout', to: 'sessions#destroy' #... end
- 次にdestroyアクションを作成します。
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController #... def destroy reset_session redirect_to root_url, notice: 'ログアウトしました。' end #... end
- 最後にアプリケーション内にログアウトのリンクを作成します。
# app/views/layouts/application.html.slim doctype html html head #... body .app-title.navbar.navbar-expand-md.navbar-light.bg-light .navbar-brand Taskleaf ul.navbar-nav.ml-auto - if current_user li.nav-item= link_to 'タスク一覧', tasks_path, class: 'nav-link' li.nav-item= link_to 'ユーザー一覧', admin_users_path, class: 'nav-link' li.nav-item= link_to 'ログアウト', logout_path, method: :delete, class: 'nav-link' - else li.nav-item= link_to 'ログイン', login_path, class: 'nav-link' .container - if flash.notice.present? .alert.alert-success= flash.notice = yield
- ログアウトの動作確認をします。
4-5-10 ログインしていなければタスク管理を利用できなくする
- before_actionを利用してタスク管理機能はログイン状態でなければ使用出来ないようにします。
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base helper_method :current_user before_action :login_required private #... def login_required redirect_to login_url unless current_user end end
- しかしこのままだとログイン画面もlogin_requiredメソッドが実行されてしまうためSessionsControllerではlogin_requiredメソッドはスキップするように設定する。
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController skip_before_action :login_required #... end
4-5-11-1 データベース上でUserとTaskを紐付ける
- 現在は全てのタスクが表示されているがこれをログインしたユーザーと関連付けされたタスクのみを表示するよう変更する。
- まずはTaskとUserの関連付けを行うためにマイグレーションを作成する。
$ rails g migration AddUserIdToTasks
# db/migrate/***_add_user_id_to_tasks.rb class AddUserIdToTasks < ActiveRecord::Migration[6.0] def up execute 'DELETE FROM taks;' add_reference :tasks, :user, null: false, index: true end def down remove_reference :tasks, :user, index: true end end
- マイグレートする。(redoでバージョンダウンも確認する)
$ rails db:migrate:redo
4-5-11-2 Railsの「関連」を定義する
- 各モデルに関連付けを定義します。
# app/models/user.rb class User < ApplicationRecord #... has_many :tasks end
# app/models/task.rb class Task < ApplicationRecord #... belong_to :user #... end
4-5-11-3 ログインしているユーザーのTaskデータの登録
- Tasksコントローラのcreateアクションを変更しユーザと関連付けされたTaskを登録するようにします。
# app/controllers/tasks_controller.rb class TasksController < ApplicationController #... def create @task = current_user.tasks.new(task_params) if @task.save redirect_to tasks_url, notice: "タスク「#{@task.name}」を登録しました。" else render :new end end #... end
4-5-11-4 ログインしているユーザーのTaskデータだけを読み出す
- Tasksコントローラのindexアクションを変更してログインしているユーザーのTaskのみを取得するようにします。
# app/controllers/tasks_controller.rb class TasksController < ApplicationController def index @tasks = current_user.tasks end #... end
- show, edit, update, destroyアクション内のTask.find(params[:id])をcurrent_user.tasks.find(params[:id])へ変更します。こうすることで他人のtaskを閲覧できなくなります。
4-5-12 管理機能を管理者ユーザだけに利用させるようにする
- 現在はどのユーザでも管理機能を使用出来てしまう。まずナビゲーションバーのユーザー一覧リンクを管理者のみ表示するよう変更します。
# app/views/layouts/application.html.slim doctype html html head #... body .app-title.navbar.navbar-expand-md.navbar-light.bg-light .navbar-brand Taskleaf ul.navbar-nav.ml-auto - if current_user li.nav-item= link_to 'タスク一覧', tasks_path, class: 'nav-link' - if current_user.admin? li.nav-item= link_to 'ユーザー一覧', admin_users_path, class: 'nav-link' li.nav-item= link_to 'ログアウト', logout_path, method: :delete, class: 'nav-link' - else li.nav-item= link_to 'ログイン', login_path, class: 'nav-link' .container #...
- Admin::Userコントローラをadminユーザのみ表示出来るようにします。
# app/controllers/admin/users_controller.rb class Admin::UsersController < ApplicationController before_action :require_admin #... private #... def require_admin redirect_to root_url unless current_user.admin? end end
4-7 タスク一覧を作成日時の新しい順に表示する
- 絞り込み条件を追加してタスク一覧を作成日時の新しい順に表示する。
# app/controllers/tasks_controller.rb class TasksController < ApplicationController def index @tasks = current_user.tasks.order(created_at: :desc) end #... end
4-8 scopeを活用する
- 例えばTaskモデルを作成日時の新しい順に表示することが多くあるとしたら一々Task.order(created_at: :desc)と書くのは煩わしい。scopeメソッドを使用することで同じ呼び出し文章を書かなくて済むようになる。
# app/models/task.rb class Task < ApplicationRecord #... scope :recent, -> { order(created_at: :desc) } #... end
- 上記のように定義すればTask.recentがTask.order(created_at: :desc)と同じ機能を果たすようになる。
4-9 フィルタを使い重複を避ける
- Tasksコントローラにはshow, edit, update, destroyアクションにそれぞれ@task = current_user.tasks.find(params[:id])が定義されている。DRYの原則に反しているため定義を1つにまとめます。
# app/controllers/tasks_controller.rb class TasksController < ApplicationController before_action :set_task, only: [:show, :edit, :update, :destroy] def show end def edit end def update task.update!(task_params) redirect_to tasks_url, notice: "タスク「#{task.name}」を更新しました。" end def destroy task.destroy redirect_to tasks_url, notice: "タスク「#{task.name}」を削除しました。" end #... private #... def set_task @task = current_user.tasks.find(params[:id]) end end
4-10 詳しい説明に含まれるURLをリンクとして表示する
- タスクの詳しい説明にURLが含まれる場合リンク化させます。rails_autolink gemを使用するのでgemを追加します。
- gemを追加したのでTasksコントローラのshowビューを編集してオートリンク化させます。
# app/views/tasks/show.html.slim #... tr th= Task.human_attribute_name(:description) td= auto_link(simple_format(h(@task.description), {}, sanitize: false, wrapper_tag: "div")) #...
- 以上で4章は終了です。