Ryoの日記

備忘ログ

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を使用した、パスワードリセット機能を実装したのですが、

その中で利用する「トークン」が聞きなれない言葉で、

遊戯王でそんなカードあったなーってレベルでした。

この記事では下記について、書いています。

  1. トークンはいつ作られてどう使われる?
  2. なぜトークンを使うのか
  3. トークンにユニーク制約をつける際の注意点

最初に状況の説明

  • UserMailerを作成
  • Userコントローラーとは別にpassword_resets_controllerを作成
  • ログイン画面からパスワードリセット画面へ遷移し、 メールアドレスを入力するとパスワードをリセットするためのurlのついたメールがuserに送られてくる。
  • メールを開きurlを踏むと、パスワードリセット申請画面に飛び、 パスワードを入力するとパスワードが新しく更新される。

1.トークンはいつ作られる?

パスワードリセット画面でメールアドレスを入力

f:id:Ryomimi:20210925054615p:plain

送信を押すと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がこれ↓

f:id:Ryomimi:20210925061048p:plain

1.トークンはいつ作られるで作られたトークンと同じトークンが使われています!

トークンはurlに埋め込んで使われてることが分かりますね!

urlを踏むとパスワードリセット画面が開きます↓

f:id:Ryomimi:20210925061423p:plain

画面が開く前に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モデルに上記の記述を加えると、新規登録をする際に下記のエラーが出るようになりました💦

f:id:Ryomimi:20210925093821p:plain

: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でやります)

  1. どこの部分をajax化するか指定する
  2. jsファイルを作る
  3. 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で文字列をセットしています。''とすれば空欄を入れることもできます。

これでサイト側からコメントの投稿を行うと f:id:Ryomimi:20210919123103p:plain

↑こんな感じで投稿を押したらコメントは消えてhogehogeがコメント欄に入りました!

ページのリロードをするとコメントも入ったと思います。

  • 流れ 投稿ボタンでデータがcontrollerに渡り、通常通りDBにコメントが保存される。

→アクションが終わるとcreate.js.erbが起動して、指定されたIDに指定された文字列が入った。(ページは更新されてないのでコメントは追加されない)

こんな感じでajax通信が出来ました。実際にはjavascriptでコメントを更新したり、削除したりすると思うので、下記のページを参考に入れてみてください!

http://semooh.jp/jquery/

カラムの追加 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 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を作成

f:id:Ryomimi:20210905202018p:plain

そして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

module SessionsHelper

def logged_in?
!@current_user.nil?
end

end


application.html.erb

<% if logged_in? %>
<li><%= link_to "Home", "/" %></li>
<li><%= link_to "About", "/about" %></li>

sessions_helperで設定しておいて、htmlの条件で使用することでhtml側の記述をhelperでまとめることができます。複数箇所で使う場合に、同じコードを描かずに済むのでより見やすいコードにすることができます。

 

2,controllerで使う

ヘルパーメソッドをcontrollerで使いたい場面もあります。

ただし、いきなりcontrollerにヘルパーメソッド内のメソッドを記述しても「そんなメソッドないよ!」と怒られてしまうので、controller内で使いたい場合は、使いたいcontrollerに`include helper名`を入れます。

class UsersController < ApplicationController
include SessionsHelper

def make
if logged_in?
#........
else
#........
end
end

今回は例でusers_controllerで記述しましたが、

全てのcontrollerで使えるようにしたい場合はapplication_controllerで記述するなどしてください。これで、controllerの各メソッド内でヘルパーメソッドが使えるようになります。

 

3,controllerでヘルパーメソッドを定義する

controllerでもviewでも頻繁に使用するメソッドがある場合に、controller内に`helper_method :メソッド名`とすることで、controller内でも使えるヘルパーメソッドとして記述することもできます。

 

class ApplicationController < ActionController::Base
helper_method :logged_in?

private
 
def logged_in?
!@current_user.nil?
end
end

今回はapplication_controllerに記述したので、全てのviewとcontrollerから呼び出せるメソッドとして使えるようになります。

 

 

 最後まで見ていただきありがとうございます。

 もし何かおかしなところや気になるところがあればコメントお願いします!