20211026のRailsに関する記事は15件です。

CRUD処理 Ruby on Rails(データベース編集方法)

データベースにおけるデータの編集や削除の方法まとめ railsのデータベースについて学んでいる時にCRUD処理について学びましたが、なかなか理解ができずまとめてみました。 ※本記事では、ageとnameが保存してあるUsersテーブル上で,データの作成、編集方法を記しました。 ・Create処理(データの追加) $ User.create(name:”A”,age:100) ・Read処理(データを読み込み) $ User.all #Usersテーブルのすべてのデータを参照 $ User.find(3) #id名からの読み込み $ User.find_by(name:”C”)  #古いものから1つ $ User.where(age:12)  #複数の検索 ・Update処理 $ user = User.find(1) #id(1)のデータをuserに代入 $ user.age = 22 #ageを22に変更 $ User.save #保存 ・Delete処理 $ user = User.find(1) #id(1)のデータをuserに代入 $ User.destroy #削除
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsチュートリアル第10章 フレンドリーフォワーディング

フレンドリーフォワーディング ログインしていないユーザーが編集ページにアクセスしようとしていたなら、ユーザーがログインした後にはその編集ページにリダイレクトされるようにする フレンドリーフォワーディングのテスト test/integration/users_edit_test.rb require 'test_helper' # test_helper.rbのパラメータ群を指定 class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "successful edit with friendly forwarding" do get edit_user_path(@user) # URLで指定したファイルの送信を要求 log_in_as(@user) # テストユーザーとしてログインする assert_redirected_to edit_user_url(@user) # リダイレクト先がどうなっているかバリデーション # edit_user_url(@user)になっているか? name = "Foo Bar" email = "foo@bar.com" # テスト情報を代入 patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } # user_path(@user)にparamasの情報を更新する # ここ編集の場合パスワードを変更しない assert_not flash.empty? # flashは表示されたか? false 空じゃない assert_redirected_to @user # リダイレクト Webサイト全体やページなどを # 新しいURLに変更したときに、 # 自動的に新しいURLに転送する仕組み # ユーザーページに転送したら本当に行っているか? @user.reload # レコードを再取得 # @userはモデル(データ) # モデルとはアプリケーションが扱うデータや処理を表現する仕組み # データを表している # レコードとは、データベース内のテーブルを構成する単位のひとつで、 # 一行分のデータを指します。 # ユーザーのデータを取得する assert_equal name, @user.name assert_equal email, @user.email # 名前とemailが変更されているかを確認 end end フレンドリーフォワーディングの実装 app/helpers/sessions_helper.rb module SessionsHelper . . . # 記憶したURL(もしくはデフォルト値)にリダイレクト def redirect_back_or(default) redirect_to(session[:forwarding_url] || default) # ||は又は # URLを指定して表示 session.delete(:forwarding_url) # カラムを削除する end # アクセスしようとしたURLを覚えておく def store_location session[:forwarding_url] = request.original_url if request.get? # GETリクエストが送られたとき end end ログインユーザー用beforeフィルターにstore_locationを追加する . . . # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? # もしも、評価が偽(false)であれば○○する store_location flash[:danger] = "Please log in." # ログインされていなければメッセージを表示する redirect_to login_url # ログインページを表示させる end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) # データベースと照らし合わせて無かったらホーム画面にいく。 end end フレンドリーフォワーディングを備えたcreateアクション app/controllers/sessions_controller.rb class SessionsController < ApplicationController def new end def create @user = User.find_by(email: params[:session][:email].downcase) # 送信されたメアドを使ってデータベースから取り出す。 #emailを小文字にする if @user &.authenticate(params[:session][:password]) # user 取得したユーザーが有効かどうか? # その後にデータベース上にパスワードがあるか? # ユーザーログイン後にユーザー情報のページにリダイレクトする # &.は省略型 log_in @user # ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成 params[:session][:remember_me] == '1' ? remember(@user) : forget(@user) # ログイン中のremember_meハッシュをオン、オフにする # 1がオン remember(user)を行う  # 0がオフ forget(user)を行う # remember(user) ダイジェストをデータベースに記憶させる redirect_back_or @user else flash.now[:danger] = 'Invalid email/password combination' # flash.nowでリクエストが発生後メッセージを消滅する # エラーメッセージを作成する render 'new' # newアクションのビューを表示 end end . . . end 演習 1. フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。次回以降のログインのときには、転送先のURLはデフォルト(プロフィール画面)に戻っている必要があります。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう . . . test "successful edit with friendly forwarding" do get edit_user_path(@user) # URLで指定したファイルの送信を要求 assert_equal session[:forwarding_url], edit_user_url(@user) #自分の編集ページかどうか log_in_as(@user) # テストユーザーとしてログインする assert_nil session[:forwarding_url] # 空かどうか? assert_redirected_to edit_user_url(@user) . . . 2. 7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください(デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう(デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって(コラム 1.2)、落ち着いて対処してみましょう)。 できなかった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsチュートリアル第10章 正しいユーザーを要求する

正しいユーザーを要求する セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。 Usersコントローラのテストを補完するように、テストを追加するところから始めていきます。 fixtureファイルに2人目のユーザーを追加する test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> 間違ったユーザーが編集しようとしたときのテスト test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? # わからない assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end end beforeフィルターを使って編集/更新ページを保護する app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] # before_actionメソッドを使って何らかの処理が実行される直前に # 特定のメソッドを実行する仕組み # ログインをさせる # :editと:updateアクションだけ . . . # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? # もしも、評価が偽(false)であれば○○する flash[:danger] = "Please log in." # ログインされていなければメッセージを表示する redirect_to login_url # ログインページを表示させる end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user # データベースと照らし合わせて無かったらホーム画面にいく。 end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 21855 Started with run options --seed 35762 35/35: [============================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.49451s 35 tests, 85 assertions, 0 failures, 0 errors, 0 skips current_user?メソッド app/helpers/sessions_helper.rb module SessionsHelper . . . # 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) # ログイン中のidを代入する # 永続セッション(ログイン中)にするため @current_user ||= User.find_by(id: user_id) # カレントユーザーまたは検索結果があったものを代入する elsif (user_id = cookies.signed[:user_id]) # user = User.find_by(id: user_id) # idでユーザーを探す if user && user.authenticated?(cookies[:remember_token]) # ユーザーが有効であり、記憶トークンも認証されたら log_in user # ログインする @current_user = user # カレントユーザーに代入するかな end end end # 渡されたユーザーがカレントユーザーであればtrueを返す def current_user?(user) user && user == current_user # 有効且つデータベースにあるか? # trueを返す end . . . end #正しいユーザーを要求する セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。 Usersコントローラのテストを補完するように、テストを追加するところから始めていきます。 fixtureファイルに2人目のユーザーを追加する test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> 間違ったユーザーが編集しようとしたときのテスト test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? # わからない assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end end beforeフィルターを使って編集/更新ページを保護する app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] # before_actionメソッドを使って何らかの処理が実行される直前に # 特定のメソッドを実行する仕組み # ログインをさせる # :editと:updateアクションだけ . . . # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? # もしも、評価が偽(false)であれば○○する flash[:danger] = "Please log in." # ログインされていなければメッセージを表示する redirect_to login_url # ログインページを表示させる end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user # データベースと照らし合わせて無かったらホーム画面にいく。 end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 21855 Started with run options --seed 35762 35/35: [============================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.49451s 35 tests, 85 assertions, 0 failures, 0 errors, 0 skips current_user?メソッド app/helpers/sessions_helper.rb module SessionsHelper . . . # 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) # ログイン中のidを代入する # 永続セッション(ログイン中)にするため @current_user ||= User.find_by(id: user_id) # カレントユーザーまたは検索結果があったものを代入する elsif (user_id = cookies.signed[:user_id]) # user = User.find_by(id: user_id) # idでユーザーを探す if user && user.authenticated?(cookies[:remember_token]) # ユーザーが有効であり、記憶トークンも認証されたら log_in user # ログインする @current_user = user # カレントユーザーに代入するかな end end end # 渡されたユーザーがカレントユーザーであればtrueを返す def current_user?(user) user && user == current_user # 有効且つデータベースにあるか? # trueを返す end . . . end #正しいユーザーを要求する セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。 Usersコントローラのテストを補完するように、テストを追加するところから始めていきます。 fixtureファイルに2人目のユーザーを追加する test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> 間違ったユーザーが編集しようとしたときのテスト test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? # わからない assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end end beforeフィルターを使って編集/更新ページを保護する app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] # before_actionメソッドを使って何らかの処理が実行される直前に # 特定のメソッドを実行する仕組み # ログインをさせる # :editと:updateアクションだけ . . . # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? # もしも、評価が偽(false)であれば○○する flash[:danger] = "Please log in." # ログインされていなければメッセージを表示する redirect_to login_url # ログインページを表示させる end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user # データベースと照らし合わせて無かったらホーム画面にいく。 end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 21855 Started with run options --seed 35762 35/35: [============================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.49451s 35 tests, 85 assertions, 0 failures, 0 errors, 0 skips current_user?メソッド app/helpers/sessions_helper.rb module SessionsHelper . . . # 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) # ログイン中のidを代入する # 永続セッション(ログイン中)にするため @current_user ||= User.find_by(id: user_id) # カレントユーザーまたは検索結果があったものを代入する elsif (user_id = cookies.signed[:user_id]) # user = User.find_by(id: user_id) # idでユーザーを探す if user && user.authenticated?(cookies[:remember_token]) # ユーザーが有効であり、記憶トークンも認証されたら log_in user # ログインする @current_user = user # カレントユーザーに代入するかな end end end # 渡されたユーザーがカレントユーザーであればtrueを返す def current_user?(user) user && user == current_user # 有効且つデータベースにあるか? # trueを返す end . . . end #正しいユーザーを要求する セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。 Usersコントローラのテストを補完するように、テストを追加するところから始めていきます。 fixtureファイルに2人目のユーザーを追加する test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> 間違ったユーザーが編集しようとしたときのテスト test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? # わからない assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end end beforeフィルターを使って編集/更新ページを保護する app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] # before_actionメソッドを使って何らかの処理が実行される直前に # 特定のメソッドを実行する仕組み # ログインをさせる # :editと:updateアクションだけ . . . # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? # もしも、評価が偽(false)であれば○○する flash[:danger] = "Please log in." # ログインされていなければメッセージを表示する redirect_to login_url # ログインページを表示させる end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user # データベースと照らし合わせて無かったらホーム画面にいく。 end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 21855 Started with run options --seed 35762 35/35: [============================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.49451s 35 tests, 85 assertions, 0 failures, 0 errors, 0 skips current_user?メソッド app/helpers/sessions_helper.rb module SessionsHelper . . . # 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) # ログイン中のidを代入する # 永続セッション(ログイン中)にするため @current_user ||= User.find_by(id: user_id) # カレントユーザーまたは検索結果があったものを代入する elsif (user_id = cookies.signed[:user_id]) # user = User.find_by(id: user_id) # idでユーザーを探す if user && user.authenticated?(cookies[:remember_token]) # ユーザーが有効であり、記憶トークンも認証されたら log_in user # ログインする @current_user = user # カレントユーザーに代入するかな end end end # 渡されたユーザーがカレントユーザーであればtrueを返す def current_user?(user) user && user == current_user # 有効且つデータベースにあるか? # trueを返す end . . . end #正しいユーザーを要求する セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。 Usersコントローラのテストを補完するように、テストを追加するところから始めていきます。 fixtureファイルに2人目のユーザーを追加する test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> 間違ったユーザーが編集しようとしたときのテスト test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? # わからない assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end end beforeフィルターを使って編集/更新ページを保護する app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] # before_actionメソッドを使って何らかの処理が実行される直前に # 特定のメソッドを実行する仕組み # ログインをさせる # :editと:updateアクションだけ . . . # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? # もしも、評価が偽(false)であれば○○する flash[:danger] = "Please log in." # ログインされていなければメッセージを表示する redirect_to login_url # ログインページを表示させる end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user # データベースと照らし合わせて無かったらホーム画面にいく。 end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 21855 Started with run options --seed 35762 35/35: [============================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.49451s 35 tests, 85 assertions, 0 failures, 0 errors, 0 skips current_user?メソッド app/helpers/sessions_helper.rb module SessionsHelper . . . # 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) # ログイン中のidを代入する # 永続セッション(ログイン中)にするため @current_user ||= User.find_by(id: user_id) # カレントユーザーまたは検索結果があったものを代入する elsif (user_id = cookies.signed[:user_id]) # user = User.find_by(id: user_id) # idでユーザーを探す if user && user.authenticated?(cookies[:remember_token]) # ユーザーが有効であり、記憶トークンも認証されたら log_in user # ログインする @current_user = user # カレントユーザーに代入するかな end end end # 渡されたユーザーがカレントユーザーであればtrueを返す def current_user?(user) user && user == current_user # 有効且つデータベースにあるか? # trueを返す end . . . end #正しいユーザーを要求する セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。 Usersコントローラのテストを補完するように、テストを追加するところから始めていきます。 fixtureファイルに2人目のユーザーを追加する test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> 間違ったユーザーが編集しようとしたときのテスト test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? # わからない assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end end beforeフィルターを使って編集/更新ページを保護する app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] # before_actionメソッドを使って何らかの処理が実行される直前に # 特定のメソッドを実行する仕組み # ログインをさせる # :editと:updateアクションだけ . . . # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? # もしも、評価が偽(false)であれば○○する flash[:danger] = "Please log in." # ログインされていなければメッセージを表示する redirect_to login_url # ログインページを表示させる end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user # データベースと照らし合わせて無かったらホーム画面にいく。 end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 21855 Started with run options --seed 35762 35/35: [============================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.49451s 35 tests, 85 assertions, 0 failures, 0 errors, 0 skips current_user?メソッド app/helpers/sessions_helper.rb module SessionsHelper . . . # 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) # ログイン中のidを代入する # 永続セッション(ログイン中)にするため @current_user ||= User.find_by(id: user_id) # カレントユーザーまたは検索結果があったものを代入する elsif (user_id = cookies.signed[:user_id]) # user = User.find_by(id: user_id) # idでユーザーを探す if user && user.authenticated?(cookies[:remember_token]) # ユーザーが有効であり、記憶トークンも認証されたら log_in user # ログインする @current_user = user # カレントユーザーに代入するかな end end end # 渡されたユーザーがカレントユーザーであればtrueを返す def current_user?(user) user && user == current_user # 有効且つデータベースにあるか? # trueを返す end . . . end #正しいユーザーを要求する セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。 Usersコントローラのテストを補完するように、テストを追加するところから始めていきます。 fixtureファイルに2人目のユーザーを追加する test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> 間違ったユーザーが編集しようとしたときのテスト test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? # わからない assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end end beforeフィルターを使って編集/更新ページを保護する app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] # before_actionメソッドを使って何らかの処理が実行される直前に # 特定のメソッドを実行する仕組み # ログインをさせる # :editと:updateアクションだけ . . . # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? # もしも、評価が偽(false)であれば○○する flash[:danger] = "Please log in." # ログインされていなければメッセージを表示する redirect_to login_url # ログインページを表示させる end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user # データベースと照らし合わせて無かったらホーム画面にいく。 end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 21855 Started with run options --seed 35762 35/35: [============================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.49451s 35 tests, 85 assertions, 0 failures, 0 errors, 0 skips current_user?メソッド app/helpers/sessions_helper.rb module SessionsHelper . . . # 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) # ログイン中のidを代入する # 永続セッション(ログイン中)にするため @current_user ||= User.find_by(id: user_id) # カレントユーザーまたは検索結果があったものを代入する elsif (user_id = cookies.signed[:user_id]) # user = User.find_by(id: user_id) # idでユーザーを探す if user && user.authenticated?(cookies[:remember_token]) # ユーザーが有効であり、記憶トークンも認証されたら log_in user # ログインする @current_user = user # カレントユーザーに代入するかな end end end # 渡されたユーザーがカレントユーザーであればtrueを返す def current_user?(user) user && user == current_user # 有効且つデータベースにあるか? # trueを返す end . . . end #正しいユーザーを要求する セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。 Usersコントローラのテストを補完するように、テストを追加するところから始めていきます。 fixtureファイルに2人目のユーザーを追加する test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> 間違ったユーザーが編集しようとしたときのテスト test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? # わからない assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end end beforeフィルターを使って編集/更新ページを保護する app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] # before_actionメソッドを使って何らかの処理が実行される直前に # 特定のメソッドを実行する仕組み # ログインをさせる # :editと:updateアクションだけ . . . # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? # もしも、評価が偽(false)であれば○○する flash[:danger] = "Please log in." # ログインされていなければメッセージを表示する redirect_to login_url # ログインページを表示させる end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user # データベースと照らし合わせて無かったらホーム画面にいく。 end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 21855 Started with run options --seed 35762 35/35: [============================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.49451s 35 tests, 85 assertions, 0 failures, 0 errors, 0 skips current_user?メソッド app/helpers/sessions_helper.rb module SessionsHelper . . . # 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) # ログイン中のidを代入する # 永続セッション(ログイン中)にするため @current_user ||= User.find_by(id: user_id) # カレントユーザーまたは検索結果があったものを代入する elsif (user_id = cookies.signed[:user_id]) # user = User.find_by(id: user_id) # idでユーザーを探す if user && user.authenticated?(cookies[:remember_token]) # ユーザーが有効であり、記憶トークンも認証されたら log_in user # ログインする @current_user = user # カレントユーザーに代入するかな end end end # 渡されたユーザーがカレントユーザーであればtrueを返す def current_user?(user) user && user == current_user # 有効且つデータベースにあるか? # trueを返す end . . . end 最終的なcorrect_userの実装 app/controllers/users_controller.rb class UsersController < ApplicationController . . . # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user # データベースと照らし合わせて無かったらホーム画面にいく。 end end 演習 1. 何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。 ユーザーでは無い人が書き換えることができるから 2. 上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか? GETメソッドのeditの方がURLを打ち込んでアクセスするのみだから editアクションらしい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsチュートリアル第10章 ユーザーにログインを要求する

認可 ウェブアプリケーションの文脈では、 認証(authentication)はサイトのユーザーを識別することであり、 認可(authorization)はそのユーザーが実行可能な操作を管理することです。 updateアクションには大きな穴がある。 ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御してみましょう。 ユーザーにログインを要求する beforeフィルターにlogged_in_userを追加する app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] # before_actionメソッドを使って何らかの処理が実行される直前に # 特定のメソッドを実行する仕組み # :editと:updateアクションだけ . . . # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? # もしも、評価が偽(false)であれば○○する flash[:danger] = "Please log in." # ログインされていなければメッセージを表示する redirect_to login_url # ログインページを表示させる end end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 17649 Started with run options --seed 49225 FAIL["test_unsuccessful_edit", #<Minitest::Reporters::Suite:0x00007f5338811400 @name="UsersEditTest">, 0.08051444600096147] test_unsuccessful_edit#UsersEditTest (0.08s) expecting <"users/edit"> but rendering with <[]> test/integration/users_edit_test.rb:13:in `block in <class:UsersEditTest>' FAIL["test_successful_edit", #<Minitest::Reporters::Suite:0x0000557d37110f08 @name="UsersEditTest">, 0.0888614100003906] test_successful_edit#UsersEditTest (0.09s) expecting <"users/edit"> but rendering with <[]> test/integration/users_edit_test.rb:32:in `block in <class:UsersEditTest>' 31/31: [============================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.12748s 31 tests, 70 assertions, 2 failures, 0 errors, 0 skips 13行目assert_template 'users/edit' 32行目assert_template 'users/edit' 表示されないらしい。 editアクションやupdateアクションでログインを要求するようになったため、ログインしていないユーザーだとこれらのテストが失敗するようになったため テストユーザーでログインする test/integration/users_edit_test.rb require 'test_helper' # test_helper.rbのパラメータ群を指定 class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do log_in_as(@user) . . . end test "successful edit" do log_in_as(@user) # ログインさせる get edit_user_path(@user) assert_template 'users/edit' name = "Foo Bar" email = "foo@bar.com" . . . end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 18369 Started with run options --seed 35897 31/31: [============================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.13056s 31 tests, 77 assertions, 0 failures, 0 errors, 0 skips getリクエストを送ってもログインしていなければページが表示されない。 セキュリティモデルを確認するためにbeforeフィルターをコメントアウトする app/controllers/users_controller.rb class UsersController < ApplicationController #before_action :logged_in_user, only: [:edit, :update] # before_actionメソッドを使って何らかの処理が実行される直前に # 特定のメソッドを実行する仕組み # ログインをさせる # :editと:updateアクションだけ . . . end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 18729 Started with run options --seed 19344 31/31: [============================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.80145s 31 tests, 77 assertions, 0 failures, 0 errors, 0 skips editとupdateアクションの保護に対するテストする test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) # テストユーザー end test "should get new" do get signup_path assert_response :success end test "should redirect edit when not logged in" do get edit_user_path(@user) assert_not flash.empty? # flashメッセージが空ではないかどうか? assert_redirected_to login_url # login_urlにリダイレクトできるか? end test "should redirect update when not logged in" do patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } # ユーザー情報を上書き assert_not flash.empty? assert_redirected_to login_url end end テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 19211 Started with run options --seed 27208 FAIL["test_should_redirect_edit_when_not_logged_in", #<Minitest::Reporters::Suite:0x0000557d39d96730 @name="UsersControllerTest">, 1.666765893000047] test_should_redirect_edit_when_not_logged_in#UsersControllerTest (1.67s) Expected true to be nil or false test/controllers/users_controller_test.rb:16:in `block in <class:UsersControllerTest>' FAIL["test_should_redirect_update_when_not_logged_in", #<Minitest::Reporters::Suite:0x0000557d39dd59d0 @name="UsersControllerTest">, 1.6791209879993403] test_should_redirect_update_when_not_logged_in#UsersControllerTest (1.68s) Expected response to be a redirect to <http://www.example.com/login> but was a redirect to <http://www.example.com/users/762146111>. Expected "http://www.example.com/login" to be === "http://www.example.com/users/762146111". test/controllers/users_controller_test.rb:27:in `block in <class:UsersControllerTest>' 33/33: [============================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.78848s 33 tests, 81 assertions, 2 failures, 0 errors, 0 skips わからない Expected true to be nil or false 16行目assert_not flash.empty? 27行目assert_redirected_to login_url 修正後 テスト ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 19244 Started with run options --seed 1774 33/33: [============================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.70367s 33 tests, 81 assertions, 0 failures, 0 errors, 0 skips 演習 1. デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです(結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか(テストが失敗するかどうか)確かめてみましょう。 ubuntu:~/environment/sample_app (updating-users) $ rails t Running via Spring preloader in process 20218 Started with run options --seed 60077 FAIL["test_should_get_new", #<Minitest::Reporters::Suite:0x00007f5338f52188 @name="UsersControllerTest">, 0.09008692899988091] test_should_get_new#UsersControllerTest (0.09s) Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/login> Response body: <html><body>You are being <a href="http://www.example.com/login">redirected</a>.</body></html> test/controllers/users_controller_test.rb:11:in `block in <class:UsersControllerTest>' FAIL["test_layout_links", #<Minitest::Reporters::Suite:0x0000557d39aac218 @name="SiteLayoutTest">, 1.471755246999237] test_layout_links#SiteLayoutTest (1.47s) Expected at least 1 element matching "title", found 0.. Expected 0 to be >= 1. test/integration/site_layout_test.rb:18:in `block in <class:SiteLayoutTest>' FAIL["test_invalid_signup_information", #<Minitest::Reporters::Suite:0x0000557d39af55f8 @name="UsersSignupTest">, 1.4869099379993713] test_invalid_signup_information#UsersSignupTest (1.49s) expecting <"users/new"> but rendering with <[]> test/integration/users_signup_test.rb:17:in `block in <class:UsersSignupTest>' FAIL["test_valid_signup_information", #<Minitest::Reporters::Suite:0x0000557d39b3f568 @name="UsersSignupTest">, 1.4974412970004778] test_valid_signup_information#UsersSignupTest (1.50s) "User.count" didn't change by 1. Expected: 2 Actual: 1 test/integration/users_signup_test.rb:22:in `block in <class:UsersSignupTest>' 33/33: [============================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.56975s 33 tests, 79 assertions, 4 failures, 0 errors, 0 skips エラーになった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ユーザー管理機能初期手順①!

①.ログイン機能の実装! ユーザー関連の機能はGemを使用します! まず、ログイン機能を実装するために、deviseというGemを使用します! ・devise ユーザー管理機能を簡単に実装するためのGemです! 実際に運用されている多くのRailsアプリケーションサービスで使用されています! Gemfileを編集します! 追記する場所は、Gemfileの最後の行です! Gemfile # 中略 gem 'devise' 次にコマンドを実行してGemをインストールします! ターミナル # Gemをインストール % bundle install 次にローカルサーバーを再起動します! Gemをインストールした後はrails sをcontrol + Cで一度停止し、サーバーを再起動する必要があります! これは、インストールしたGemの反映するタイミングが、サーバー起動時だからです! ターミナル # Gemをインストール後 % rails s 次のステップへ行きます! ②.deviseの設定ファイルを作成! deviseを使用するためには、Gemのインストールに加え、 devise専用のコマンドで設定ファイルを作成する必要があります! ・rails g devise:installコマンド このコマンドは、追加したdeviseというGemの「設定関連に使用するファイル」を自動で生成するコマンドです! ターミナル # deviseの設定ファイルを作成 % rails g devise:install 以下のようなログが流れれば成功です! ターミナル create config/initializers/devise.rb create config/locales/devise.en.yml =============================================================================== Depending on your application's configuration some manual setup may be required: 1. Ensure you have defined default url options in your environments files. Here is an example of default_url_options appropriate for a development environment in config/environments/development.rb: config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } In production, :host should be set to the actual host of your application. * Required for all applications. * 2. Ensure you have defined root_url to *something* in your config/routes.rb. For example: root to: "home#index" * Not required for API-only Applications * 3. Ensure you have flash messages in app/views/layouts/application.html.erb. For example: <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> * Not required for API-only Applications * 4. You can copy Devise views (for customization) to your app by running: rails g devise:views * Not required * =============================================================================== *deviseのバージョンによって出力結果が異なる場合があります! これでファイルは作成されます! 次のステップに行きます! ③.deviseのUserモデルを作成! deviseを利用する際には、ユーザー情報を管理するためのUserモデルを新しく作成する必要があります! 作成には通常のモデルの作成方法ではなく、deviseのモデル作成用コマンドでUserモデルを作成します! 下記の指示に従い、ログイン機能を持つUserモデルを作成しましょう! ・rails g deviseコマンド rails g deviseコマンドは、deviseによるユーザー機能の対象を指定することで、モデルとマイグレーションの生成やルーティングの設定などをまとめて処理します! 実行すると、モデルが生成され、routes.rbにはdeviseに関連するパスが追加されます! rails g deviseコマンドでuserを指定します! ターミナル # deviseコマンドでUserモデルを作成 % rails g devise user 実行して以下のようなログが出力されれば成功です! ターミナル invoke active_record create db/migrate/20200309082300_devise_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml insert app/models/user.rb route devise_for :users ユーザーに関する、モデルやマイグレーションも自動生成されています! また、routes.rbに以下のルーティングが自動的に追記されます! config/routes.rb Rails.application.routes.draw do devise_for :users <-この部分が自動的に追記される root to: 'eats#index' resources :eats end devise_forは、ユーザー機能に必要な複数のルーティングを一度に生成してくれるdeviseのメソッドです! *モデルとマイグレーションは作成されましたが、まだusersテーブルは作成されていません。 ④.テーブルを作成! 先ほど自動生成されたマイグレーションファイルを使用して、テーブルを作成します! テーブルを作成する前に、どのようなテーブルを作成するのか確認します! 先ほどのrails g devise userコマンドで作成されたマイグレーションファイルを開きます! db/migrate/20XXXXXXXXX_devise_create_users.rb (例)config/routes.rb class DeviseCreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| ## Database authenticatable t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at # 省略 t.timestamps null: false end add_index :users, :email, unique: true add_index :users, :reset_password_token, unique: true # add_index :users, :confirmation_token, unique: true # add_index :users, :unlock_token, unique: true end end マイグレーションファイルを確認すると、すでに記述がある項目があります! db/migrate/20XXXXXXXXX_devise_create_users.rb t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" メールアドレスのカラムを作成する記述があり、パスワードのカラムを作成する記述もあります! db/migrate/20XXXXXXXXX_devise_create_users.rb t.timestamps null: false レコードの作成日時と更新日時を管理するためのカラムです! こちらはdeviseに関係なく、マイグレーションファイルが生成された際に、初期状態で記述してあります! ここまでで、テーブルの設計が確認できたので、マイグレーションを実行します! ターミナル # マイグレーションを実行 % rails db:migrate これで、usersテーブルが作成されました! Sequel Proでusersテーブルを確認し、usersテーブルが作成されていれば成功です! テーブル・カラム情報を変更したため、ローカルサーバーを再起動します! ターミナル ローカルサーバーを起動 % rails s これでlocalhostへアクセスしても問題なく動作します! ⑤.まとめ 簡単に言うと! deviseとは、ユーザー管理機能を簡単に実装するためのGemのこと! rails g devise:install コマンドとは、deviseというGemの設定関連に使用するファイルを自動で生成するコマンドのこと! rails g deviseコマンドとは、deviseの中で使用するユーザーモデルclass名を置き換え、modelなど必要なファイルを生成するコマンドのこと! と言う感じだと思います! 手順通りやっていけば問題ないと思います! 次回続きを記述していきます! 何か説明で間違っていたらご指導お願い致します(_ _)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails,Cropper,CarrierWave,MiniMagickで画像を任意の位置でトリミングして登録するまで

やりたいこと 画像を任意の位置でトリミングして登録したい。 (下のgifではユーザー登録時に画像をトリミングして登録→詳細画面) 使用ツール Rails 6.1.4.1 CarrierWave 2.2.2(ファイルのアップロード機能を簡単に追加する事が出来るgem) MiniMagick 4.11.0(バックエンドで画像をリサイズ、トリミング出来るgem) Cropper.js(フロントエンドで画像のトリミングが出来るJavaScriptライブラリ) 実現するためのプロセス ①フロントエンドの処理 Cropper.jsを導入することでトリミング画面の実装及びトリミング位置を取得できる。 Cropper.js側で下図のx,y,h,wの数値を取得し、viewのhiddenタグに各値を設定。 ②バックエンドの処理 MiniMagickをincludeしたCarrierWaveのImageUploaderモデル内でフロントから 送られてきたx,y,h,wをもとに画像をトリミングする。 ソースの解説 user.rb # -------------------------------- 解説1 ---------------------------------- class User < ApplicationRecord mount_uploader :image, ImageUploader attr_accessor :image_x attr_accessor :image_y attr_accessor :image_w attr_accessor :image_h attr_accessor :aspect_numerator attr_accessor :aspect_denominator # 省略 end # ------------------------------------------------------------------------ ■解説1 imageはCarrierWaveでアップロードするためImageUploaderをマウントする。 その他トリミングに使用する値はDBに登録するわけではないので仮想属性を使用する。 new.html.erb <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>登録画面</title> <!-------------------------------- 解説1 ----------------------------------> <link rel="stylesheet" type="text/css" media="all" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.css" /> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.js"></script> <!------------------------------------------------------------------------> </head> <body> <div class="form_wrapper"> <h1>登録画面</h1> <%= form_with model: @user, url: "/users", method: "post" do |f| %> <!-------------------------------- 解説2 ----------------------------------> <div id="image-wrapper"> <%= image_tag "default.png", id: :prev_img %> <canvas id="cropped_canvas" style="display:none"></canvas> </div> <%= f.file_field :image, id: :trim_img_uploder, value: "assets/default.png" %> <!------------------------------------------------------------------------> <div class="field"> <%= f.label :username, "氏名" %> <%= f.text_field :username, value: @user.username %> </div> <div class="field"> <%= f.label :email, "メールアドレス" %> <%= f.text_field :email, value: @user.email %> </div> <!-------------------------------- 解説3 ----------------------------------> <%= f.hidden_field :image_x, id: "image_x" %> <%= f.hidden_field :image_y, id: "image_y" %> <%= f.hidden_field :image_w, id: "image_w" %> <%= f.hidden_field :image_h, id: "image_h" %> <%= f.hidden_field :aspect_numerator, id: "aspect_numerator", value: "1.0" %> <%= f.hidden_field :aspect_denominator ,id: "aspect_denominator", value: "1.0" %> <button type='submit'>登録</button> <!------------------------------------------------------------------------> <% end %> <!-------------------------------- 解説4 ----------------------------------> <div id="modal_area"> <div id="modal_back_area" class="modal_back_area"></div> <div class="modal_wrapper"> <div class="modal_padding modal_title_wrapper"> <h4>範囲を選択してください</h4> </div> <div class="canvas_wrapper"> <td><canvas id="source_canvas" width="1" height="1"></canvas></td> </div> <button type="button" id="close_button">OK</button> </div> </div> </div> <!------------------------------------------------------------------------> </body> </html> ■解説1 今回はCDNでCropper.jsを読み込むため記載を忘れないようにする。 ■解説2 image_tagでは画像選択前にデフォルトで表示する画像を指定する。 canvasのcropped_canvasではcropper.jsでトリミング後の画像を描画する。 そのためcanvasには"display:none"を指定し、画像が選択された後にjs側で表示する。 ■解説3 バックエンド処理時に使用する値をhiddenで設定する。 ■解説4 画像選択時に表示するモーダルウィンドウ。 canvasのsource_canvasではcropper.jsでトリミング前の画像を描画する。 users_controller.rb class UsersController < ApplicationController def new @user = User.new() end def create @user = User.new(user_params) if @user.save redirect_to user_path(@user) else # エラー時の処理 end end def show @user = User.find_by(:id => params[:id]) end # -------------------------------- 解説1 ---------------------------------- private def user_params attrs = [ :username, :email, :image_x, :image_y, :image_w, :image_h, :aspect_numerator, :aspect_denominator, :image ] params.require(:user).permit(attrs) end # ------------------------------------------------------------------------ end ■解説1 viweで設定したhidden値をストロングパラメーターに設定する。 imageよりも前にトリミング時に使用する仮想属性を記載すること。 前に記載しないと仮想属性に値が入っていない状態でImageUploader側の処理が走り 正常にトリミングされない。 image_uploader.rb class ImageUploader < CarrierWave::Uploader::Base # -------------------------------- 解説1 ---------------------------------- include CarrierWave::MiniMagick process resize_to_fit: [500, 500] # ------------------------------------------------------------------------ storage :file def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # -------------------------------- 解説2 ---------------------------------- version :cropped do process :crop end private def crop manipulate! do |img| crop_x = model.image_x.to_i crop_y = model.image_y.to_i crop_w = model.image_w.to_i crop_h = model.image_h.to_i img.crop "#{crop_w}x#{crop_h}+#{crop_x}+#{crop_y}" img end end # ------------------------------------------------------------------------ end ■解説1 MiniMagickをincludeする。(MiniMagickのメソッドを使えるようにするため。) resize_to_fitは指定した数値をもとにリサイズするメソッド。 ここでcrop_image.jsで指定しているscaled_widthで同一の大きさにリサイズしないと トリミング位置がずれるため注意。 ■解説2 cropメソッド内で登録対象の画像に対してトリミング処理を行っている。 manipulate! do |img|は対象となる画像を取り出すおまじない。 modelは今回の場合user、image_x,y,w,hはuserモデルの仮想属性として受け取っているため ここで呼び出すことができる。 img.cropはMiniMagickで使用できるメソッドでソースの書き方でトリミングできる。 crop_image.js document.addEventListener("turbolinks:load", function(){ //-------------------------------- 解説1 ---------------------------------- $('#trim_img_uploder').click(function(e){ $(this).val(''); document.getElementById("prev_img").style.display = ''; document.getElementById("cropped_canvas").style.display = 'none'; }); $('#trim_img_uploder').change(function(e){ document.getElementById("prev_img").style.display = 'none'; document.getElementById("cropped_canvas").style.display = ''; $('#modal_area').fadeIn(); }); //-------------------------------- 解説2 ---------------------------------- let cropper = null; const scaled_width = 500; const aspect_numerator = parseFloat(document.getElementById("aspect_numerator").value) const aspect_denominator = parseFloat(document.getElementById("aspect_denominator").value) const crop_aspect_ratio = aspect_denominator / aspect_numerator; //-------------------------------- 解説3 ---------------------------------- const crop_image = function (e) { const files = e.target.files; if (files.length == 0) { return; } let file = files[0]; let image = new Image(); let reader = new FileReader(); reader.readAsDataURL(file); reader.onload = function (e) { image.src = e.target.result; image.onload = function () { //-------------------------------- 解説4 ---------------------------------- let scale = scaled_width / image.width; const canvas = document.getElementById("source_canvas"); canvas.width = image.width * scale; canvas.height = image.height * scale; let ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height); if (cropper != null) { cropper.destroy(); } //-------------------------------- 解説5 ---------------------------------- cropper = new Cropper(canvas, { aspectRatio: crop_aspect_ratio, data: {width: canvas.width, height: canvas.width * crop_aspect_ratio}, crop: function (event) { document.getElementById("image_x").value = event.detail.x; document.getElementById("image_y").value = event.detail.y; document.getElementById("image_w").value = event.detail.width; document.getElementById("image_h").value = event.detail.height; } } ); //-------------------------------- 解説6 ---------------------------------- $('#close_button,#modal_back_area').click(function(){ const cropped_canvas = document.getElementById("cropped_canvas"); let ctx = cropped_canvas.getContext("2d"); let cropped_image_width = image.height * crop_aspect_ratio; cropped_canvas.width = cropped_image_width * scale; cropped_canvas.height = image.height * scale; let image_x = document.getElementById("image_x").value; let image_y = document.getElementById("image_y").value; let image_w = document.getElementById("image_w").value; let image_h = document.getElementById("image_h").value; ctx.drawImage(image, image_x/scale, image_y/scale, image_w/scale , image_h/scale ,0 ,0 , cropped_canvas.width ,cropped_canvas.height); $('#modal_area').fadeOut(); }); } } } // アップローダーに画像が設定されるとcrop_imageを設定 const uploader = document.getElementById('trim_img_uploder'); uploader.addEventListener('change', crop_image); }); ■解説1 アップローダーに画像が設定された場合の挙動を記載している。 まず.clickでアップローダークリック時valueを空にする。 この処理がないと同じ画像が二度連続で設定された際モーダルが開かない。 .changeではアップローダーに画像が設定された際、もともと表示していたデフォルト画像を 非表示に設定し、トリミング後の画像表示領域を表示に設定する。その後モーダルを表示する。 ■解説2 scaled_widthはモーダル内に表示する画像の大きさを定義している。 そのためここがImageUploader側でリサイズする大きさと異なるとトリミング位置が ずれてしまうため注意。 crop_aspect_ratioは画像の縦横比を指定している。 縦横比が固定の場合はjsにべた書きしても良い。 ■解説3 crop_imageはアップローダーに画像が設定されると呼ばれる関数式。 FileReaderオブジェクトはローカルのBlobやFileオブジェクトが保有するバッファの中身に、 読み取りアクセスを行う事ができる。 readAsDataURLメソッドで受け取ったfileの情報を読み取る。 onloadイベントはfileの読み込み完了後に実行したい処理を記載する。 ■解説4 モーダル内に画像を描画する。 scaleで元の画像サイズと描画するサイズのスケールの%を割り出す。 canvasに描画するdrawImageメソッドでは元の画像サイズと描画サイズ両方を引数として 渡す必要がある。 canvas.getContext("2d")ではcanvasから2Dグラフィックに特化した情報を取得でき、 そこで得たctxに対してdrawImageメソッドを使用できる。 ■解説5 トリミング領域を作成する。 new Cropper(トリミング対象のcanvas, トリミングのオプション)によりモーダル内の画像に対してトリミング領域を作成する。 今回はオプションは最低限のものを指定。(さらに必要な場合は公式ドキュメント参照) cropオプションではトリミング領域を拡大縮小する度にx,y,w,hの値をhiddenタグのvalueに 設定する。 ■解説6 トリミング後の画像を描画する。 モーダルのOKボタンを押したことをトリガーにトリミング後の画像を描画する。 解説4同様drawImageメソッドを使用しての描画であるため説明は割愛する。 show.thml.erb #省略 <div class="form_wrapper"> <h1>ユーザー詳細</h1> <div id="image-wrapper"> <%= image_tag "#{@user.image.cropped}", id: :prev_img %> </div> <p><%= @user.username %></p> </div> 蛇足かもしれないが念のため。 public/uploadsフォルダ配下を確認してほしいが、トリミング前の画像とトリミング後の画像が両方保管されている。 トリミング後の画像を表示するには@user.image.croppedで呼び出すことができる。 最後に Ruby勉強中であるため改善点やもっと良い方法などあればご教授願いたいです。 参考文献 以下記事太字の記事に関しましては大変参考にさせていただきました。 ありがとうございました。 ■Cropper.js関連 Cropper.js で画像を切り出してCanvasに描画するサンプル https://puarts.com/?pid=1483 CarrierwaveとMiniMagickでユーザー任意の入力値で画像をトリミングする https://qiita.com/mochikana/items/fb7a4d4488efd5bb5ae1 公式ドキュメント https://github.com/fengyuanchen/cropperjs/blob/main/README.md Cropper.jsを使ってみる https://cly7796.net/blog/javascript/try-using-cropper-js/ ■FileReader関連 FileReader.onload https://developer.mozilla.org/ja/docs/Web/API/FileReader/onload FileReader クラスについて https://hakuhin.jp/js/file_reader.html#FILE_READER_00 ■CarrierWave関連 CarrierWave+MiniMagickで使う、画像リサイズのメソッド https://qiita.com/wann/items/c6d4c3f17b97bb33936f 【Rails】CarrierWave・MiniMagickで画像をトリミングできない問題 https://qiita.com/1060ki/items/d48fe26a784380630f54 ■その他 canvasのgetContext("2d")って何 https://qiita.com/manten120/items/86c087b937708697acec input file に同じファイルonchangeが発火されない https://qiita.com/Anders/items/8cdb7fc392556b275d85
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

omniauth-samlでログイン後にセッションが消えて大変だった

omniauthを既に使っていてたため、omniauth-samlを使ってSAML対応することになったけど、SAMLの承認自体は通るもののその後セッションが持続せず毎回ログアウトされるようになっていた。 結論 omniauth_callbacks_controller.rb protect_from_forgery except: [:saml] 自前のstrategyを持っていたりしてどこでtokenが消失するのかとかを追っていたけど、単純にprotect_from_forgeryで消されていただけだった。 今まで使っていたomniauth-google-oauth2がexceptしなくても動いていたので気づくのに時間がかかった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】日付連番のデータを大量に作成したい

環境 Ruby 3.0.2 Rails 6.1.4.1 成功 前提:userは定義済み、Postテーブルにdateカラムが存在する。 dateのループの中でデータ作成する spec.rb (Date.new(2021, 1, 1)..Date.new(2021, 9, 1)).each do |date| create(:post, date: date, user: user) end 失敗 FactoryBotのdateカラムを定義する際にdateをループして factory.rb FactoryBot.define do factory :post do date { (Date.new(2021, 1, 1)..Date.new(2021, 9, 1)).each do |date| date end } end end テストの方で200個作成 spec.rb ... create_list(:post, 200, user: user) ... すると 「レンジを引用できない」エラー TypeError: can't quote Range
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コメントに対する返信機能の作成

はじめに 自作アプリで以下のようなコメントに対する返信機能を実装した。コメント機能はともかく、初心者の私に返信機能は難しかったのでまとめておく。Railsのバージョンは5.2.6である。         なお、以下の2つの記事にお世話になりました。ありがとうございます。 How to create a comment and reply system in Ruby on Rails Railsでのコメント返信機能の実装 そもそも返信はコメントとどう違うの? コメントのうち、parent_idを持つものを返信と判断する。parent_idを持たないただのコメントもあるので、アソシエーション設定時はbelongs_toにoptional: trueをつけることに注意する。 models/post_comments.rb class PostComment < ApplicationRecord belongs_to :parent, class_name: 'PostComment', optional: true has_many :replies, class_name: 'PostComment', foreign_key: :parent_id, dependent: :destroy end ER図でいうと下図のように、自分のカラムが自分に繋がってるイメージか。           なお、ルーティングはコメントを投稿の中にネストさせている。 config/routes.rb Rails.application.routes.draw do resources :posts do resources :post_comments, only: [:new, :create, :destroy] end end 返信機能の実装①:コメント機能について 前提として、非同期通信によるコメント機能は出来上がっているものとする。つまり、投稿詳細ページにあるコメント入力フォームからコメントを送信すると、post_commentsコントローラのcreateアクションによってcreate.js.erbがよびだされてコメントが表示されるものとする。 controllers/posts_controller.rb def show @post = Post.find(params[:id]) @post_comment = PostComment.new end controllers/post_comments_controller.rb def create @post = Post.find(params[:post_id]) @post_comment = PostComment.new(post_comment_params) @post_comment.post_id = @post.id @post_comment.user_id = current_user.id @post_comment.save end private def post_comment_params params.require(:post_comment).permit(:comment, :parent_id) end views/posts/show.html.erb <% if user_signed_in? %> <%= form_with model:[@post, @post_comment] do |f| %> <div class="form-group"> <%= f.text_area :comment, rows: 4, class: "col-9 form-control mt-3", placeholder: "コメントをここに" %> </div> <div class="actions"> <%= f.submit 'コメントする',class: "btn btn-secondary rounded-pill" %> </div> <% end %> <% end %> <div class="post_comments"> <%= render "post_comments/index", post: @post %> </div> views/post_comments/create.js.erb $(".post_comments").html("<%= j(render 'post_comments/index', post: @post) %>"); $("textarea").val('') コメント一覧の表示は部分テンプレート_index.html.erbに含めている。 views/post_comments/_index.html.erb <% post.post_comments.order(created_at: :desc).each do |post_comment| %> <% if !post_comment.parent_id %> <div class="card mt-3"> <div class="row"> <div class="col-md-2 ml-3 mr-0 my-3"> <%= link_to user_path(post_comment.user) do %> <%= attachment_image_tag post_comment.user, :profile_image, fallback: "no_image.png", size: "35x35" %><br> <%= post_comment.user.name %> <% end %> </div> <div class="col-md-6"> <div class="card-body"> <p><%= post_comment.comment %></p> </div> </div> <div class="col-md-3"> <p class="mt-3 mb-0"><%= post_comment.created_at.strftime("%Y/%m/%d %H:%M") %></p><br> <% if user_signed_in? %> <%= link_to '返信', new_post_post_comment_path(post, parent_id: post_comment.id), remote: true %> <% end %> <% if current_user == post_comment.user %> <%= link_to '削除', post_post_comment_path(post_comment.post_id, post_comment.id), method: :delete, remote: true %> <% end %> </div> </div> </div> <% end %> <% if post_comment.replies.any? %> <% post_comment.replies.each do |reply| %> <%= render partial: 'post_comments/reply', locals: { reply: reply } %> <% end %> <% end %> <div id="reply-form-<%= post_comment.id %>"></div> <% end %> _index.html.erbの上部はコメントをeach文で表示しているだけだが、注意すべきは二行目で<% if !post_comment.parent_id %>としている点である。これがないと、返信もコメントと同じように表示されてしまう。  下部では返信一覧と返信フォームを表示しているが、返信フォームの表示部分id="reply-form-<%= post_comment.id %>を返信一覧の下に書いていることがポイントである。 返信機能の実装②:コメントにparent_idを持たせる方法  上述のように、ただのコメントと返信であるコメントの違いはparent_idを持つかどうかである。_index.html.erbの中の返信ボタンにもう一度注目してみる。 views/post_comments/_index.html.erb <%= link_to '返信', new_post_post_comment_path(post, parent_id: post_comment.id), remote: true %> 返信ボタンを押すと、post_comments コントローラのnewアクションがよばれ、非同期通信なのでnew.js.erbがよびだされる。この時、parent_idがparams[:parent_id]として渡されることに注意する。newアクションとnew.js.erbは次のようになる。 controllers/post_comments_controller.rb def new @post = Post.find(params[:post_id]) @post_comment = @post.post_comments.new(parent_id: params[:parent_id]) end views/post_comments/new.js.erb document.querySelector("#reply-form-<%= @post_comment.parent_id %>").innerHTML = ("<%= j(render 'post_comments/reply_form', post_comment: @post_comment) %>"); new.js.erbでは_index.html.erb中のid="reply-form-<%= post_comment.id %>の中身を右辺で置き換えることをしている。では、parent_idを持った@post_comment、要するに返信はどのように表示されるのか。 返信機能の実装③:返信の表示 返信フォームを表示する_reply_form.html.erbは以下の通り。 views/post_comments/_reply_form.html.erb <div class="ml-5"> <%= form_with(model: [@post, @post_comment]) do |f| %> <%= f.hidden_field :parent_id %> <div class="form-group"> <%= f.label '返信' %> <%= f.text_area :comment, class: 'form-control', placeholder: "返信をここに" %> </div> <%= f.submit '送信', class: 'btn btn-primary rounded-pill' %> <%= link_to 'キャンセル', post_path(@post), class: 'btn btn-secondary rounded-pill' %> <% end %> </div>  冒頭のml-5クラスは重要である。これがあることで、あるコメントに紐づいた返信をしていることが視覚的に明らかになる。また、hidden_fieldでparent_idを送っていることにも注意する。送信ボタンを押すとpost_commentsコントローラのcreateアクションによって返信であるコメントとしてDBに保存される。  _index.html.erbから分かるように、返信一覧は特定のコメントの下に表示される。返信一覧を表示する_reply.html.erbは以下の通りである。ここでも、冒頭のml-5クラスが視覚的に重要な役割を果たしている。 views/post_comments/_reply.html.erb <div class="card ml-5"> <div class="row"> <div class="col-md-1 m-auto"> <%= image_tag "arrow.png", size: '40x40' %> </div> <div class="col-md-2 mx-auto mt-3"> <%= link_to user_path(reply.user) do %> <%= attachment_image_tag reply.user, :profile_image, fallback: "no_image.png", size: "30x30" %><br> <%= reply.user.name %> <% end %> </div> <div class="col-md-6"> <div class="card-body"> <%= reply.comment %> </div> </div> <div class="col-md-3 mt-4"> <p><%= reply.created_at.strftime("%Y/%m/%d %H:%M") %></p> </div> </div> </div> 最後に  この返信機能で使ったようなアソシエーションは、冒頭でも紹介した参考記事によると自己結合型(self-joining)のアソシエーションというようです。分かれば簡単ですが、部分テンプレートが入り組んでいて少し複雑でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]初心者でほぼぶっつけ本番で映画レビューサイトを作ってみた件

前置き もう3年近く前の話で、すごい今更で記憶もちょっと曖昧ですけど、記録としてまとめてみようと思いました(笑) 就活と学校の進級制作展兼コンテストのために作ったものなので、結構力を入れました かなり昔に一度だけRuby on Railsを触ったことがあるが、その時はまだjava初心者レベルでhtml/cssやhttpやデータベースなどの知識も一切なかったので、全く理解できず諦めました。 今回は学校の授業でhtml/cssを習って、http関連知識やSQL構文などを少し触ったおかけで、チュートリアルを読みながら書いてましたが、思ったより早いペースで順調に進めることができました。 完全に自分の作品なので、試してみたいことをとりあえず一通り詰め込んでみましたw 画面 URL https://moasis.herokuapp.com/ (パソコン画面のために作ったので、スマホ画面のレイアウトは超軽く調整しただけ) 管理者アカウント:julia@example.com/password ホームページ (どうでもいい情報:名前はMOVIE+OASISの組み合わせによる造語、ロゴも合わせて映画のイメージと木のの組み合わせ設計しましたw) 使用した映画DB及びrailsのAPI利用ツール こちらの記事でまとめました https://qiita.com/julia817/items/522da21edee1bf69994b 詰まってたところ チュートリアルのuserモデルとSNSアカウントでログインする機能が衝突によるエラーが発生 こちらの記事でまとめました https://qiita.com/julia817/items/7908c01984a291b74966 ページネーション チュートリアル通りに試してみましたが、will_paginate gemは確か配列に対応しないので、うまくいかなかったです 調べてみた結果、kaminari gemを使用することにしました 全然記憶がないけど、ぱっと見bootstrapとかは多分これぽい記事を参考したと思います データベースの構築 完全の初心者でマークした映画リストをどうやってユーザと結びつくとかも全然わからなくて、映画をサンプルした記事もほどんとなかったので、音楽リストなどの記事を色々調べて真似してみたらうまく行きましたw (プレゼンのために軽く図を書いたけど、保存しなかったかも..もし見つかったらまた追記する) 画像アップロード なんかリサイズとか色々全然うまくいかなくて、結構詰まってましたが、時間もあんまりないので、最低限のアップロード機能しか実装してないです。本番S3に切り替えるのも諦めて、プロフィール画像アップロードしても30分後ぐらいにデフォルト画像に戻ります おすすめツール グラフ(chartkick) https://chartkick.com/ 一行で簡単にグラフを作れる カウンターキャッシュ(counter_culture) https://github.com/magnusvk/counter_culture 高度なカウンターキャッシュ機能で、「いいね」カウンターとして使いました SNS共有(addthis) https://www.addthis.com/ 2行でSNS共有ボタンを追加できるツール 最後に 一応ソースコードも公開します:https://github.com/julia817/moasis ご参考まで... とにかく試しに一つの成果物を完成させたいという感覚でしたので、コードの細かいところは全然気にしてなかったし、バグや未完成の部分はもう完全に放置状態です(笑) とにかく作品作りの達成感と爽快感が最高です!! これからも頑張って行きます!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails.application.secrets.secret_key_baseをherokuの本番環境で適用する

本番環境でRails.application.secrets.secret_key_baseが原因で本番環境でjwtを使うことができなかったのでメモを残します。 エラーの原因調査 エラーのログ heroku logs -tail 2021-10-25T12:59:16.093104+00:00 heroku[router]: at=info method=OPTIONS path="/v1/accounts" host=api-slack-react-ruby.herokuapp.com request_id=b010205a-d1b1-4436-aa04-ad84e944e2ea fwd="153.246.218.124" dyno=web.1 connect=0ms service=1ms status=200 bytes=287 protocol=https 2021-10-25T12:59:16.273962+00:00 heroku[router]: at=info method=GET path="/v1/accounts" host=api-slack-react-ruby.herokuapp.com request_id=0e2bccc5-9a32-4ccf-9689-e8e375ea8556 fwd="153.246.218.124" dyno=web.1 connect=0ms service=3ms status=500 bytes=374 protocol=https 2021-10-25T12:59:16.271652+00:00 app[web.1]: I, [2021-10-25T12:59:16.271587 #4] INFO -- : [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] Started GET "/v1/accounts" for 153.246.218.124 at 2021-10-25 12:59:16 +0000 2021-10-25T12:59:16.272513+00:00 app[web.1]: I, [2021-10-25T12:59:16.272471 #4] INFO -- : [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] Processing by V1::AccountsController#callback as HTML 2021-10-25T12:59:16.273001+00:00 app[web.1]: I, [2021-10-25T12:59:16.272917 #4] INFO -- : [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] Completed 500 Internal Server Error in 0ms (ActiveRecord: 0.0ms | Allocations: 130) 2021-10-25T12:59:16.273322+00:00 app[web.1]: F, [2021-10-25T12:59:16.273291 #4] FATAL -- : [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] 2021-10-25T12:59:16.273322+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] JWT::DecodeError (No verification key available): 2021-10-25T12:59:16.273323+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] 2021-10-25T12:59:16.273323+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] lib/j_w_t/helper.rb:12:in `decode' 2021-10-25T12:59:16.273324+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] app/models/concerns/j_w_t/authenticatable.rb:23:in `authenticate!' 2021-10-25T12:59:16.273325+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] app/controllers/concerns/authenticatable.rb:15:in `authenticate_account!' でみてみると 2021-10-25T12:59:16.273322+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] JWT::DecodeError (No verification key available): ここらへんが原因だと思う 該当するコードが以下 helper.rb def decode(token, verify: true, **opts) JWT.decode(token, Rails.application.secrets.secret_key_base, verify, **opts) end 原因としてはRails.application.secrets.secret_key_baseが怪しいと思ったので、調査してみると。。。 heroku run bash rails c Rails.application.secrets.secret_key_base やはりnilが返ってきていた。 すこし原因調査が長くなってしまったが、ここから解決していこうと思う 試した方法 rails consoleでRails.application.secrets.secret_key_baseの値を取得後に、heorkuの環境変数にSECRET_KEY_BASEをセット ↓ 変わらず。。。 herokuの環境変数にRAILS_MASTER_KEYをセット heroku config:set RAILS_MASTER_KEY=cat config/master.key ↓ 変わらず。。。 Rails.application.secret.secret_key_baseをRails.application.credentials.secret_key_baseに変更した helper.rb def decode(token, verify: true, **opts) JWT.decode(token, Rails.application.secrets.secret_key_base, verify, **opts) end helper.rb def decode(token, verify: true, **opts) JWT.decode(token, Rails.application.credentials.secret_key_base, verify, **opts) end ↓ うまく行った!!! 結論 herokuの環境変数にRAILS_MASTER_KEYをセットし、Rails.application.secret.secret_key_baseをRails.application.credentials.secret_key_baseに変更するとうまく行った! うまく行った理由を調べてみると、Railsは5.1から秘匿情報をsecret.ymlではなく、credentials.yml.encで管理するようになったらしい。 そのため、secret_key_baseの参照先もsecretsからcredentialsに変わったということだろう。 また、秘匿情報を見るためにはmaster keyを環境変数にセットする必要があるので2.で試したことも有効だったと考えられる。 かなり時間を使ってしまったが、解決することができてよかった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails secret_key_baseをherokuの本番環境で適用する

本番環境でRails.application.secrets.secret_key_baseが原因で本番環境でjwtを使うことができなかったのでメモを残します。 エラーの原因調査 エラーのログ heroku logs -tail 2021-10-25T12:59:16.093104+00:00 heroku[router]: at=info method=OPTIONS path="/v1/accounts" host=api-slack-react-ruby.herokuapp.com request_id=b010205a-d1b1-4436-aa04-ad84e944e2ea fwd="153.246.218.124" dyno=web.1 connect=0ms service=1ms status=200 bytes=287 protocol=https 2021-10-25T12:59:16.273962+00:00 heroku[router]: at=info method=GET path="/v1/accounts" host=api-slack-react-ruby.herokuapp.com request_id=0e2bccc5-9a32-4ccf-9689-e8e375ea8556 fwd="153.246.218.124" dyno=web.1 connect=0ms service=3ms status=500 bytes=374 protocol=https 2021-10-25T12:59:16.271652+00:00 app[web.1]: I, [2021-10-25T12:59:16.271587 #4] INFO -- : [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] Started GET "/v1/accounts" for 153.246.218.124 at 2021-10-25 12:59:16 +0000 2021-10-25T12:59:16.272513+00:00 app[web.1]: I, [2021-10-25T12:59:16.272471 #4] INFO -- : [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] Processing by V1::AccountsController#callback as HTML 2021-10-25T12:59:16.273001+00:00 app[web.1]: I, [2021-10-25T12:59:16.272917 #4] INFO -- : [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] Completed 500 Internal Server Error in 0ms (ActiveRecord: 0.0ms | Allocations: 130) 2021-10-25T12:59:16.273322+00:00 app[web.1]: F, [2021-10-25T12:59:16.273291 #4] FATAL -- : [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] 2021-10-25T12:59:16.273322+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] JWT::DecodeError (No verification key available): 2021-10-25T12:59:16.273323+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] 2021-10-25T12:59:16.273323+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] lib/j_w_t/helper.rb:12:in `decode' 2021-10-25T12:59:16.273324+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] app/models/concerns/j_w_t/authenticatable.rb:23:in `authenticate!' 2021-10-25T12:59:16.273325+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] app/controllers/concerns/authenticatable.rb:15:in `authenticate_account!' でみてみると 2021-10-25T12:59:16.273322+00:00 app[web.1]: [0e2bccc5-9a32-4ccf-9689-e8e375ea8556] JWT::DecodeError (No verification key available): ここらへんが原因だと思う 該当するコードが以下 helper.rb def decode(token, verify: true, **opts) JWT.decode(token, Rails.application.secrets.secret_key_base, verify, **opts) end 原因としてはRails.application.secrets.secret_key_baseが怪しいと思ったので、調査してみると。。。 heroku run bash rails c Rails.application.secrets.secret_key_base やはりnilが返ってきていた。 すこし原因調査が長くなってしまったが、ここから解決していこうと思う 試した方法 rails consoleでRails.application.secrets.secret_key_baseの値を取得後に、heorkuの環境変数にSECRET_KEY_BASEをセット ↓ 変わらず。。。 herokuの環境変数にRAILS_MASTER_KEYをセット heroku config:set RAILS_MASTER_KEY=cat config/master.key ↓ 変わらず。。。 Rails.application.secret.secret_key_baseをRails.application.credentials.secret_key_baseに変更した helper.rb def decode(token, verify: true, **opts) JWT.decode(token, Rails.application.secrets.secret_key_base, verify, **opts) end helper.rb def decode(token, verify: true, **opts) JWT.decode(token, Rails.application.credentials.secret_key_base, verify, **opts) end ↓ うまく行った!!! 結論 herokuの環境変数にRAILS_MASTER_KEYをセットし、Rails.application.secret.secret_key_baseをRails.application.credentials.secret_key_baseに変更するとうまく行った! うまく行った理由を調べてみると、Railsは5.1から秘匿情報をsecret.ymlではなく、credentials.yml.encで管理するようになったらしい。 そのため、secret_key_baseの参照先もsecretsからcredentialsに変わったということだろう。 また、秘匿情報を見るためにはmaster keyを環境変数にセットする必要があるので2.で試したことも有効だったと考えられる。 かなり時間を使ってしまったが、解決することができてよかった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初学者向け】部分テンプレートとエラー表示の相性が良い件

はじめに Railsを学習し始めた頃は部分テンプレートに苦手意識がありましたが、ネットで見つけた、部分テンプレートを用いたエラー表示の書き方を見て理解が深まったので共有します。 背景 例えば、ユーザーや本に関する投稿ができるアプリがあるとして、最初のうちはユーザーと本それぞれの新規投稿画面new.html.erbの上部に以下のようなエラー文を書いていました。 books/new.html.erb <% if @book.errors.any? %> <%= @book.errors.count %>件のエラーが発生しました <% @book.errors.full_messages.each do |message| %> <%= message %> <% end %> <% end %> users/new.html.erb <% if @user.errors.any? %> <%= @user.errors.count %>件のエラーが発生しました <% @user.errors.full_messages.each do |message| %> <%= message %> <% end %> <% end %> しかし、これだとDRYの原則にも反しますし、編集画面edit.html.erbにも書くとなると面倒です。 解決策 エラー表示を以下のように部分テンプレート化する方法を知りました。まず、errorsフォルダ下に_errors.html.erbファイルを用意します。 _errors.html.erb <% if obj.errors.any? %> <%= obj.errors.count %>件のエラーが発生しました <% obj.errors.full_messages.each do |message| %> <%= message %> <% end %> <% end %> この書き方の優れている点は、objという(中性的なイメージの?)ローカル変数を用いることで、objに@bookや@userなどさまざまなインスタンス変数を渡すことができる点です。 例えば、本の新規投稿画面なら、@book = Book.newというインスタンス変数をobjに渡すことで、 books/new.html.erb <%= render "errors/errors", obj:@book %> と書けますし、ユーザーの編集画面なら、@user = User.find(params[:id])というインスタンス変数をobjに渡すことで、 users/edit.html.erb <%= render "errors/errors", obj:@user %> と書くことができます。このようにできるのも、エラー表示が様々なモデルに共通して同じ形式であるおかげです。 結論 今では当たり前となりましたが、当初は、部分テンプレートのローカル変数に@bookや@userなどの異なるモデルからなる様々なインスタンス変数を渡してエラー表示するやり方に目が覚める思いで、部分テンプレートの真髄を見たような気がしました。エラー表示以外にも多くの画面で共通のもの(ヘッダー、フッターはもちろん他にも何かありそう)は積極的に部分テンプレートを使っていきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

部分テンプレートとエラー表示の相性が良い件

はじめに Railsを学習し始めた頃は部分テンプレートに苦手意識がありましたが、ネットで見つけた、部分テンプレートを用いたエラー表示の書き方を見て理解が深まったので共有します。 背景 例えば、ユーザーや本に関する投稿ができるアプリがあるとして、最初のうちはユーザーと本それぞれの新規投稿画面new.html.erbの上部に以下のようなエラー文を書いていました。 books/new.html.erb <% if @book.errors.any? %> <%= @book.errors.count %>件のエラーが発生しました <% @book.errors.full_messages.each do |message| %> <%= message %> <% end %> <% end %> users/new.html.erb <% if @user.errors.any? %> <%= @user.errors.count %>件のエラーが発生しました <% @user.errors.full_messages.each do |message| %> <%= message %> <% end %> <% end %> しかし、これだとDRYの原則にも反しますし、編集画面edit.html.erbにも書くとなると面倒です。 解決策 エラー表示を以下のように部分テンプレート化する方法を知りました。まず、errorsフォルダ下に_errors.html.erbファイルを用意します。 _errors.html.erb <% if obj.errors.any? %> <%= obj.errors.count %>件のエラーが発生しました <% obj.errors.full_messages.each do |message| %> <%= message %> <% end %> <% end %> この書き方の優れている点は、objという(中性的なイメージの?)ローカル変数を用いることで、objに@bookや@userなどさまざまなインスタンス変数を渡すことができる点です。 例えば、本の新規投稿画面なら、@book = Book.newというインスタンス変数をobjに渡すことで、 books/new.html.erb <%= render "errors/errors", obj:@book %> と書けますし、ユーザーの編集画面なら、@user = User.find(params[:id])というインスタンス変数をobjに渡すことで、 users/edit.html.erb <%= render "errors/errors", obj:@user %> と書くことができます。このようにできるのも、エラー表示が様々なモデルに共通して同じ形式であるおかげです。 結論 今では当たり前となりましたが、当初は、部分テンプレートのローカル変数に@bookや@userなどの異なるモデルからなる様々なインスタンス変数を渡してエラー表示するやり方に目が覚める思いで、部分テンプレートの真髄を見たような気がしました。エラー表示以外にも多くの画面で共通のもの(ヘッダー、フッターはもちろん他にも何かありそう)は積極的に部分テンプレートを使っていきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存アプリのN+1問題の解消

はじめに 既存アプリにおいてActiveStorageで画像を取得する際発生していたN+1問題を改善したため、これを機にN+1問題についてまとめてみました。 実行環境 ・macOS ・Rails 6.1.4 ・Ruby 2.6.7 N+1問題とは モデルに対してeach文などで繰り返しを行う時、そのモデルに紐付く要素に対してモデルの数分SQLクエリが発行されてしまう問題のこと。レコード数が多くなってしまうとレスポンスが遅くなるなどの影響が出てしまう。 自分のアプリで実際にN+1問題が起きていた箇所を例に見ていきます。 shop.rb class Shop < ApplicationRecord belongs_to :user end userがshopを投稿するため上の関連付けになっています shops_controller.rb class ShopsController < ApplicationController def index @shops = Shop.all end end shopレコードは上のコードで取得します。 viewコード <% if current_user?(shop.user) %> <div class="manipulate_shop"> <%= link_to icon("far", "edit"), edit_shop_path(shop), class: "edit_icon" %> | <%= link_to icon("far", "trash-alt"), shop, method: :delete, data: { confirm: "本当に削除しますか?" }, class: "trash_icon" %> </div> <% end %> ログインユーザーが店舗の投稿主かどうかでレイアウトを変えるコードです。eachがありませんがshopを繰り返し読み込んでいます。 このviewコードを読み込む際N+1問題が以下のように発生します。 ターミナル SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]] SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]] SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 3], ["LIMIT", 1]] shopに紐付くuserの数だけSQLを発行しています。 このままではレコードが多くなるほどSQLも多く発行されるので、将来的にレスポンスが遅くなってしまいます。 そのため、コントローラでshopを取得するときにincludesを用いて一緒にそのshopを投稿したuserも取得します。 shops_controller.rb class ShopsController < ApplicationController def index @shops = Shop.all.includes(:user) end end 以下がincludesを使った後のSQLクエリです。一回の問い合わせでレコードを取得してくれるようになりました。 IN (1, 2, 3)は WHERE "users"."id" = 1 OR "users"."id" = 2 OR "users"."id" = 3と同じでusersテーブルからidが1か2か3がtrueになるものでテーブルを作成しています。 ターミナル SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3) [["id", 1], ["id", 2], ["id", 3]] これでN+1問題が解消されました。 ActiveStorageでのN+1問題 shopモデルに画像を紐づけるためにActiveStorageを使いimagesとしてアタッチしました。 shop.rb class Shop < ApplicationRecord belongs_to :user has_many_attached :images end 先ほどのshops_contorollerと同じindexアクションから取得した@shopsを使って以下のコードを書きました。 画像が紐づいているかどうかで画像表示を変えています。このshop.images.firstの部分でN+1問題が発生しました。 viewコード <%= link_to shop do %> <% if shop.images.attached? %> <%= image_tag shop.images.first, class: "card-img" %> <% else %> <%= image_tag "noimage.png", class: "card-img" %> <% end %> <% end %> 以下が発行されたSQLクエリです。N+1問題が発生しております。 ターミナル SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 ORDER BY "active_storage_attachments"."id" ASC LIMIT $4 [["record_id", 3], ["record_type", "Shop"], ["name", "images"], ["LIMIT", 1]] SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2 [["id", 5], ["LIMIT", 1]] SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 ORDER BY "active_storage_attachments"."id" ASC LIMIT $4 [["record_id", 2], ["record_type", "Shop"], ["name", "images"], ["LIMIT", 1]] SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]] SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 ORDER BY "active_storage_attachments"."id" ASC LIMIT $4 [["record_id", 1], ["record_type", "Shop"], ["name", "images"], ["LIMIT", 1]] SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]] ActiveStorageの場合はwith_attached_つけた名前でN+1問題を解消できます。shopレコードに関連するactive_storage_attachmentsとactive_storage_blobsをまとめて取得してくれます。 shops_controller class ShopsController < ApplicationController def index @shops = Shop.all.includes(:user).with_attached_images end end 以下が改善後のSQLクエリです。今回の場合、改善後により多くのデータを取得してしまっていますがSQLクエリの回数が抑えられています。 ターミナル SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" = $1 AND "active_storage_attachments"."name" = $2 AND "active_storage_attachments"."record_id" IN ($3, $4, $5) [["record_type", "Shop"], ["name", "images"], ["record_id", 3], ["record_id", 2], ["record_id", 1]] SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" IN ($1, $2, $3, $4, $5, $6, $7, $8) [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5], ["id", 6], ["id", 7], ["id", 8]] 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む