RSpec render :newでurlが思ったのと違う
ハマったことのメモ。
RSpecでuserの新規作成あたりのsystemspecを作成していた時の事。
User_Controllerはこんな感じ
def create @user = User.new(user_params) if @user.save redirect_to login_path, notice: 'User was successfully created.' else render :new end
user作成失敗時の挙動のテストで、render :new
だから
期待するurlはnew_user_url
だな〜と思って記入するとあってるはずなのにエラー。
Failure/Error: expect(current_path).to eq new_user_path expected: "/users/new" got: "/users"
どうやらurlが違ったみたい
rails routes
で確認すると
users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy
"/users"はindexじゃない?
と思いつつとりあえず期待するurlをusers_pathとすると成功!!謎!
ここから色々調べた結果こんな質問が↓
https://teratail.com/questions/135588
render 'new'は、newアクションを呼び出しません。表示されるビューだけを切り替えています。
createアクションは/usersなので、これがRailsのデフォルトの動作です。エラーで戻った先を/users/newにしたければ、あえてリダイレクトなどを実装する必要があります。
createアクションのurlが残るからrender時の期待するurlは/usersでいいらしい。
全然気にしてなかったので新しい発見でした。
sorcery パスワードリセット〜トークンの流れ〜
Railsでsoceryのreset_passwordを使用した、パスワードリセット機能を実装したのですが、
その中で利用する「トークン」が聞きなれない言葉で、
遊戯王でそんなカードあったなーってレベルでした。
この記事では下記について、書いています。
最初に状況の説明
- UserMailerを作成
- Userコントローラーとは別にpassword_resets_controllerを作成
- ログイン画面からパスワードリセット画面へ遷移し、 メールアドレスを入力するとパスワードをリセットするためのurlのついたメールがuserに送られてくる。
- メールを開きurlを踏むと、パスワードリセット申請画面に飛び、 パスワードを入力するとパスワードが新しく更新される。
1.トークンはいつ作られる?
- トークン発行までの流れ *
パスワードリセット画面でメールアドレスを入力
送信を押すとpasswordresetsコントローラーのcreateが走る
class PasswordResetsController < ApplicationControlle def create @user = User.find_by(email: params[:email]) @user&.deliver_reset_password_instructions! redirect_to login_path, success: 'パスワードリセット手順を送信しました' end
この中の@user&.deliver_reset_password_instructions!メソッドが実行した時点で binding.pryで止めると、Userカラムのreset_password_tokenに文字列が入ってきたのがわかると思います。 (一番下の行あたり)
7: def create 8: @user = User.find_by(email: params[:email]) 9: binding.pry => 10: @user&.deliver_reset_password_instructions! 11: redirect_to login_path, success: 'パスワードリセット手順を送信しました' 12: end [1] pry(#<PasswordResetsController>)> next User Update All (3.9ms) UPDATE "users" SET "reset_password_token" = 'YLpP-sF4xiH1Y1KYpypN', "reset_password_email_sent_at" = '2021-09-25 05:45:05.692469' WHERE "users"."id" = ? [["id", 2]]
パスワードリセット申請画面でメールアドレスを入力&送信した時点でトークンを発行したことがわかると思います。 次からそのトークンがどう使われるかみていきます。
トークンはどう使われる?
先程の@user&.deliver_reset_password_instructions!メソッドで、 トークンを作ったのと同時にUserMailerを使ってリセットの案内メールを送っています。
class UserMailer < ApplicationMailer def reset_password_email(user) # このメソッドを使用 @user = User.find(user.id) @url = edit_password_reset_url(@user.reset_password_token) #<=ここでトークンを使用 mail( to: user.email, subject: 'パスワードリセット') end end
password_resetsコントローラーのcreateで定義した@userを引数にして、 reset_password_emailメソッドを呼び出します。 ここではメールのview側で使用するために@userと@urlを定義しています。
urlを定義する際に引数にトークンを使用しています。
<h1>パスワードリセット</h1> <p> <%= @user.last_name %><%= @user.first_name %> 様 パスワード再発行のご依頼を受け付けました。 こちらのリンクからパスワードの再発行を行ってください。 <p><a href="<%= @url %>"><%= @url %></a> </p>
メール側ではこんな感じに@urlを使用
できたurlがこれ↓
1.トークンはいつ作られる
で作られたトークンと同じトークンが使われています!
トークンはurlに埋め込んで使われてることが分かりますね!
urlを踏むとパスワードリセット画面が開きます↓
画面が開く前にeditアクションを通過してます↓
def edit @token = params[:id] @user = User.load_from_reset_password_token(params[:id]) return not_authenticated if @user.blank? end
ここではurlに記載されていたトークンを@tokenへ入れています。 また、トークンを発行したメールアドレスの持ち主を@userに入れています。
もしトークンがいじられていたりして、トークンがデータベースから見つからない場合は not_authenticatedが実行されるようになっています。(pageが開かないようになってる)
こうやって、urlからパスワードリセットしたいユーザーをトークンを使用して認識しています。
こうして開いたパスワードリセット画面で、新しくパスワードを入力すると、 パスワードがアップデートされます。
class PasswordResetsController < ApplicationController def update @token = params[:id] @user = User.load_from_reset_password_token(params[:id]) #<=ユーザーの認識 return not_authenticated if @user.blank? #<=もし確認が取れなかったらreturn @user.password_confirmation = params[:user][:password_confirmation] #<=password_confirmationに格納 if @user.change_password(params[:user][:password]) #<=パスワードをアップデートするメソッド redirect_to(root_path, notice: 'パスワード変更に成功しました') else flash.now[:danger] = 'パスワード変更に失敗しました' render :edit end end
ここまでがトークンの流れでした。
トークンはなぜ使われる?
もしトークンを使わずに001などの番号で識別していたとすると、 パスワード更新用のurlが下のようになります。
http://localhost:3000/password_resets/001/edit
ITの知識がある人だと、001の部分を002にすれば2番目のユーザーのパスワードを 弄れるのではないかと気付かれてしまう可能性があります。
そこをトークンにすることで、連想できなくさせることができるので、 トークンを使用しています。
つまり、悪用を防ぐためのトークンだったのですね。
トークンにユニーク制約をつける際の注意点
トークンは自動生成されるため、万が一同じトークンが生成されたとすると パスワードリセットで他人のパスワードを変更してしまう可能性があります。
可能性はないに等しいですが、一応トークンカラムにユニーク特性を付けておいた方が、 万が一も防ぐことができます。↓
user.rb class User < ApplicationRecord #省略 validates :reset_password_token, uniqueness: true end
userモデルに上記の記述を加えると、新規登録をする際に下記のエラーが出るようになりました💦
:reset_password_token
カラムはパスワードリセットしてないuserには基本的にnilが入っています。
新しくユーザー登録しようとするとnil
が既にあるのでuniquenessのバリデーションに引っかかっていることが原因でした。
これを解決するために、nilを許容する記述を加えます。
user.rb class User < ApplicationRecord #省略 validates :reset_password_token, uniqueness: true, allow_nil: true end
これでエラー解決できました!
以上、トークンについて学んだことのまとめです。 最後までご覧いただきありがとうございました。
ajax化の前知識
railsを学習している中で「javascriptを使ってボタンのajax化をする」という課題が出ました。
基礎の流れがよくわかってなかったので、ajax通信で変更する場所はどうやって決める?jqueryとかDOMとかどういうこと?というところがよくわからずに困ったので、
そういう状況の方の1歩になればと思って記事を書きます。
まず ajax化とは
通常の流れは↓サーバーとやりとり時間があるので、一旦画面が白くなるのが特徴。
①ブラウザからクリックなどでサーバーへリクエスト送信
②サーバー側で処理して場合によってデータ生成→サーバーへレスポンスを返す(htmlや画像)
③ブラウザ(表示)
ajax化...javascriptを使用して、画面の一部のみ変更することで画面移管せずに表示を変更することができる。
①ブラウザからクリックなどでサーバーへリクエストを送信(javascriptで必要な部分のみをリクエスト)
②リクエストが返ってくる間の待ち時間も操作が可能
③リクエストが返ってきたら必要な部分のみを変更するので画面は白くならない
ということができる技術のようです。
ちなみにajax通信を調べてて出てきた用語を調べて理解したこと↓
DOM...htmlのどこの部分を更新するかを指定している
JQuery...javascriptのライブラリ(なんのこっちゃ) →htmlのDOM操作とajax処理をjavascriptを使って簡単に記述できるようにしている便利な道具(的なイメージ)
なんとなくわかった気がする...
続いて、実際に組み込む方法をやってみます!
railsで実際に使用してみる(form_withでやります)
- どこの部分をajax化するか指定する
- jsファイルを作る
- jsファイルに記述して動作確認
1.どこの部分をajax化するか指定する
・状況
commentの_formから投稿する機能を作っている状況から、ajax通信を使ってみる。 formからsubmitした時にcommentコントローラーからcreateアクションに飛ぶように設定している。
今回は、form_withを使用するとします。
form_withでは通常の通信であればlocal: true
を指定しますが、ajax通信を使ったリクエストであれば、local: true
の記述は削除します
<%= form_with model: comment do |f| %> <%= f.label :body %> <%= f.text_area :body ,id: 'js-new-comment-body', %> <%= f.submit '投稿',class: 'btn btn-primary' %> <% end %>
上記のようなフォームを使用するとします(form_withの他のパラメーターは適当なのでご勘弁)
local: true
を削除することで「ajax通信を使うから、controller側に飛んだ後は'アクション名.js.erb'ファイルに飛んでね」と指示することができます。
2.jsファイルの作成
view/comments/create.js.erbというファイルを作成します。
createアクション内で使用しているredirect_to
を削除します。
そうすることでcreate.js.erbに飛んでくれるようになります。
いよいよjavascriptを記述します!
3.javascriptを記述
例えば、create.js.erb内にこのような記述をするとどうなるか
$("#js-new-comment-body").val('hogehoge')
$("#js-new-comment-body")
←この部分はどこを変更するかを指定しています。あらかじめview側でidを決めておく必要があります。
また、後からみてわかりやすいように頭にjsをつけておくと良いです。
.val('hogehoge')←この部分はどういう動作をさせるかを決めています。今回はval
で文字列をセットしています。''
とすれば空欄を入れることもできます。
これでサイト側からコメントの投稿を行うと
↑こんな感じで投稿を押したらコメントは消えてhogehoge
がコメント欄に入りました!
ページのリロードをするとコメントも入ったと思います。
- 流れ 投稿ボタンでデータがcontrollerに渡り、通常通りDBにコメントが保存される。
→アクションが終わるとcreate.js.erbが起動して、指定されたIDに指定された文字列が入った。(ページは更新されてないのでコメントは追加されない)
こんな感じでajax通信が出来ました。実際にはjavascriptでコメントを更新したり、削除したりすると思うので、下記のページを参考に入れてみてください!
カラムの追加 rails
dbのカラムの追加について、 毎回調べてはよく忘れるのでメモとして。
カラムの追加
rails g migration Addカラム名Toテーブル名 カラム名:型
例えば、TaskテーブルにBodyカラムを追加するとすると、
taskleaf % rails g migration AddBodyToTask body:text #<=ターミナルで実行する行 Running via Spring preloader in process 8994 invoke active_record create db/migrate/20210912032753_add_body_to_task.rb #<=生成されるファイル
こんな感じでファイルが生成される。
db/migrate/20210912033209_add_body_to_task.rb↓
class AddBodyToTask < ActiveRecord::Migration[5.2] def change add_column :tasks, :body, :text end end
このままでは設定ファイルを作っただけなので、実行します。実行コマンドは↓
taskleaf % rails db:migrate #<=実行コマンド == 20210912033209 AddBodyToTask: migrating ==================================== -- add_column(:tasks, :body, :text) -> 0.0010s
これで設定してなかったdbファイル全ての設定が一括で実行されます。
rails-i18n 翻訳されない事件
rails s
で再起動かけてもダメ、
インデントを穴が開くほど確認してもダメ、、
パスが通ってないか確認しても合ってるはずなのにダメ、、、
ハゲる!!ってなったので誰かの手助けになればと思い、書きます。
今回の状況
application.rb
module TaskDo class Application < Rails::Application config.i18n.default_locale = :ja #アプリのデフォルトを日本語に指定 config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s] #locales以下にパスが通るように指定 end end
localesにactiverecord(model用)とview(view&controller用)という名前のファイルを作成
→それぞれにja.ymlを作成
そしてview用のja.ymlに翻訳を書き込んでちゃんと表示されるか確認してみることに。
config/local/view/ja.yml
ja: defaults: login: 'ログイン' register: '登録'
対象のview
<div class="actions"> <%= f.submit "登録",class: 'btn btn-primary' %> </div>
結果
...表示されない💩
gemの問題か...?→rails s
で再起動かけてもダメ、
じゃあviewでミスってるかja.ymlのよくあるインデントミスかな、
→view側は特に記述ミスはなさそう、、
→インデントを穴が開くほど確認してもダメ(これだけしか記述してないので確認するとこないやん、、)
パスの通し方を確認しても合ってるはずなのにダメ、、、
そしてググりにググって探し当てたのが「他のja.yml」が空だと翻訳されないという質問箱の内容。。
そんなパターンあるんか?と思いつつ、
local/activerecord/ja.ymlに
ja:
これだけ記述すると、、表示された!
まじか!!この3文字で俺の朝の30分.....orz
最後までご覧いただき有り難うございました。笑
この記事がもし誰かのエラーの手助けになれば、、、
root to:について
root to: について
ルーティングに使用するroot to:のパスの指定の仕方をすっかり忘れてたので、メモ。
root to:の使い方
routes.rbにて
root to: 'static_pages#top'
こんな感じで指定してhttp://localhost:3000に接続すると static_pagesコントローラーのtopアクションに対応するページが開くようになる。
root to:のヘルパーメソッドは?
view側からlink_toメソッドなどでtopを呼びたい場合のurl指定は"/"ですが、
ヘルパーメソッドで指定する場合は
、下の様にroot_path
で指定できます。
<%= link_to "トップページ" root_path class: 'navbar-brand' %>
色々試してると、こんな感じで省略してもトップページに飛ぶことを発見。
<%= link_to "トップページ" class: 'navbar-brand' %>
helperメソッドをview・controllerで使う
helperメソッドの使い方
ヘルパーメソッドの使い方をちゃんと理解できていなかったので備忘録として残します。
目次
1,viewでヘルパーメソッドを使う
2,controllerでヘルパーメソッドを使う
3,controllerでヘルパーメソッドを定義する
1,viewで使う
Railsのhelper.rbはviewで使うメソッドを集めたモジュール(メソッドの塊)のイメージです。
helper.rbで作っておけば、どのviewからも使うことが可能になります。
例えば、、
sessions_helper.rb
application.html.erb
sessions_helperで設定しておいて、htmlの条件で使用することでhtml側の記述をhelperでまとめることができます。複数箇所で使う場合に、同じコードを描かずに済むのでより見やすいコードにすることができます。
2,controllerで使う
ヘルパーメソッドをcontrollerで使いたい場面もあります。
ただし、いきなりcontrollerにヘルパーメソッド内のメソッドを記述しても「そんなメソッドないよ!」と怒られてしまうので、controller内で使いたい場合は、使いたいcontrollerに`include helper名`を入れます。
今回は例でusers_controllerで記述しましたが、
全てのcontrollerで使えるようにしたい場合はapplication_controllerで記述するなどしてください。これで、controllerの各メソッド内でヘルパーメソッドが使えるようになります。
3,controllerでヘルパーメソッドを定義する
controllerでもviewでも頻繁に使用するメソッドがある場合に、controller内に`helper_method :メソッド名`とすることで、controller内でも使えるヘルパーメソッドとして記述することもできます。
今回はapplication_controllerに記述したので、全てのviewとcontrollerから呼び出せるメソッドとして使えるようになります。
最後まで見ていただきありがとうございます。
もし何かおかしなところや気になるところがあればコメントお願いします!