20210321のRailsに関する記事は30件です。

Active Recordのpluckとselect

違い

  • pluckはArrayを返す。selectはActiveRecord::Relationを返す。
  • pluckがメソッドチェーンの最後に書かれているのなら、selectを使った時と、発行されるクエリは変わらないはず。
> puts User.select(:id).to_sql
SELECT "users"."id" FROM "users"

# pluck(:id)は、Arrayを返すので、to_sql()をチェーンすることはできない
> puts User.pluck(:id)
(4761.4ms)  SELECT "users"."id" FROM "users"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveRecord preload, eager_load, include, joinについて

ActiveRecord preload, eager_load, include, joinについて、以下の記事が一番分かりやすい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsのコントローラー上で.envを使った環境変数を読み込めなかった時の対応

実現したいこと:
Railsのコントローラー上で、
.envのファイルに設定した環境変数を読み込んでAPIを取得したい。

環境:
Ruby 2.6.6
Rails 6.0.3.4
gemのdotenv-railsをインストール済

状況:
Ruby-on-RailsでSNSのアプリケーションを作成中にNewsApiを使用し、
Newsの情報を取得し表示する機能を実装していた。
APIKEYを.envファイルにて環境変数に置き換えて使用したかったが、
エラーが発生して読み込めなかった。

ニュース機能の実装で参考にした記事:
https://qiita.com/UTOG/items/f6438420e81b6488a508
上記執筆者様の実装方法を参考にNewsApiへ登録し、
APIKEYを取得し環境変数として設定後、記載されている実装コードを活用しファイル作成。(ニュースカテゴリー等の設定は変更)

対象ファイル:

app/controller/news_Controller.rb
class NewsController < ApplicationController
  def index
  end

  def data
    uri = URI.parse('http://newsapi.org/v2/top-headlines?country=jp&category=business&pageSize=15&apiKey=<%="#{ENV['API_KEY']}"%>')
    json = Net::HTTP.get(uri)
    moments = JSON.parse(json)
    @data = moments['articles'].to_json
  end
end
.env
API_KEY='この中に取得したAPI_KEYを入力'

上記コードでは下記のようなエラーが発生。

エラー文:
syntax error, unexpected tCONSTANT, expecting ')'
...ze=15&apiKey=<%="#{ENV['API_KEY']}"%>')
... ^~~~~~~
/app/controllers/news_controller.rb:6: syntax error, unexpected ')', expecting end
...iKey=<%="#{ENV['API_KEY']}"%>')

調べたところ閉じタグがないと警告が出ており、
どうやら環境変数が読み込めていないため、
そこでコードの読み込みが止まってしまっているようあった。

試したこと:
1.環境変数を挿入していた該当のURLに取得したAPIKEYをベタ打ちで挿入
→ニュースが問題なく表示される。

2.コンソール上で環境変数を実行( 実行コード:$ ENV['API_KEY'] )
→.env内に記載していたAPIKEYが問題なく表示される。

→この時点で、
①ニュースを表示すること
②.env内での環境変数の設定はできていると予想。
コントローラーへの環境変数の組み込み方に問題があるのではと仮定。

2.環境変数の挿入方法を変更
(news.controller.rb 6行目を下記のパターンで変更 ※対象箇所周辺コードのみ抜粋)

country=jp&category=business&pageSize=15&apiKey=<%="#{ENV['API_KEY']}"%>')
country=jp&category=business&pageSize=15&apiKey=<%=ENV['API_KEY']%>')
country=jp&category=business&pageSize=15&apiKey=<"#{ENV['API_KEY']}"')
country=jp&category=business&pageSize=15&apiKey=ENV['API_KEY']')

→検索しながら上記色々なパターンを実行したものの、エラー等で読み込めない状況は変わらず。。

原因:
<% %>や<%= %>の記述はerbファイル内で使える記述であり、
erb上でRubyコードを実行したり、Rubyで定義した変数などをHTMLとして出力したりする際に利用するとのことであり、コントローラー内では記述方法は異なるとのこと。

解決策:
下記、コードを修正。
ダブルクォーテーション(")とシングルクォーテーション(')の使い方にも間違いがあった模様。

app/controller/news_Controller.rb
class NewsController < ApplicationController
  def index
  end

  def data
    uri = URI.parse("http://newsapi.org/v2/top-headlines?country=jp&category=business&pageSize=15&apiKey=#{ENV['API_KEY']}")
    json = Net::HTTP.get(uri)
    moments = JSON.parse(json)
    @data = moments['articles'].to_json
  end
end

今回、エラーにハマっていたところを質問サイトにて回答を頂いたため、
同じ状況の方のご参考になればと思い、共有させて頂きます。

まだまだ未熟なため説明不足な点もございますが、
今後修正等加えて参りますので気になる点がございましたらぜひ、
ご指導、ご鞭撻のほどよろしくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tips: Rubyでresponse bodyをstreamで受け取ってNoMemoryErrorを回避する

RubyやRailsなどでHTTPリクエストを行う場合に、response bodyをstreamで受け取る方法を紹介します。
単純にリクエストをするとレスポンスの結果を全てメモリに載せてしまうので、メモリの節約や巨大なデータを取得するとNoMemoryErrorが発生してしまいます。
今回の手法はこのエラーになってしまう状態を回避する方法として有効です。

Net::HTTPの場合

にある通り、read_bodyというメソッドを用いることでstreamでデータの受け取りを行うことができます。ファイルに書き出すことでメモリを圧迫せずにデータを取得できます。

require 'net/http'

uri = URI.parse('http://example.com/')
Net::HTTP.start(uri.host, uri.port) do |http|
  http.request_get(uri.path) do |response|
    File.open("file_path", "w") do |file|
      response.read_body do |chunk|
        file.write(chunk)
      end
    end
  end
end

また、streamで取得したデータの累計サイズを記録しておくことで、特定のサイズでデータの取得を打ち切るようなこともできます。

require 'net/http'

uri = URI.parse('http://example.com/')
response_body = ''
total_size = 0
Net::HTTP.start(uri.host, uri.port) do |http|
  http.request_get(uri.path) do |response|
    response.read_body do |chunk|
      response_body += chunk if total_size <= 1000000 # 1Mバイトまでのデータまで受け取る
      total_size += chunk.bytesize
    end
  end
end

Faradayの場合

にある通り、on_dataというオプションにprocを渡すことでread_bodyと同じようなことができます(このオプションが使えるのはNet::HTTPをアダプタにしている場合のみです)

require 'faraday'

connection = Faraday.new(url: 'http://example.com/')
response_body = ''
connection.get('/') do |request|
  request.options.on_data = proc do |chunk, overall_received_bytes|
    puts "Received #{overall_received_bytes} characters"
    response_body += chunk
  end
end

また、Net::HTTPと同じようにファイルに書き出すこともできますし、切り捨ても行えます。

require 'faraday'

connection = Faraday.new(url: 'http://example.com/')
connection.get('/') do |request|
  request.options.on_data = proc do |chunk, overall_received_bytes|
    puts "Received #{overall_received_bytes} characters"
    File.open("file_path", "w") do |file|
      file.write(chunk)
    end
  end
end
require 'faraday'

connection = Faraday.new(url: 'http://example.com/')
response_body = ''

connection.get('/') do |request|
  request.options.on_data = proc do |chunk, overall_received_bytes|
    puts "Received #{overall_received_bytes} characters"
    response_body += chunk if overall_received_bytes <= 1000000 # 1Mバイトまでのデータまで受け取る
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails sして無限ループから抜けられなくなった時の解決法【超初心者】

redirect_toしたら永遠にクルクル回ってしまった!!!

コンソール
$ps -ef | grep puma

するとec2-user 1234 23456 82 10:26 pts/3 00:04:41 puma 8.11.7 (localhost:2000)みたいなのが出てくる

コンソール
kill -9 1234
#書いてある数字

したらpumaがストップしました、、、
良かったです、、、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]コントローラのbefore_actionでインスタンス変数を定義するのはやめよう

前提

この記事で書いているインスタンス変数は全てview側で参照しているものとします。

まずは結論から

↓のように、view側で使うインスタンス変数をbefore_actionで初期化するのはやめましょう。

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit]

  def show; end

  def edit; end

  private

  def set_post
    @post = Post.find params[:id]
  end
end

なぜ何故よくないのか

view側でインスタンス変数が呼び出されてるのを見たとき、普通は真っ先にコントローラーのアクションを見に行きます。
その時にアクションの中でインスタンス変数が初期化されてない場合、どこで初期化されたか探す必要が出るためコードの可読性が下がります。
↑のコードはまだ一目見てわかるようになっていますが、例えば、

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit]

  def show; end

  def edit; end

  # ここに行数が長いコード

  private

  # ここに行数が長いコード

  def set_post
    @post = Post.find params[:id]
  end
end

このように行数の長いコードが挟まってると、画面には収まりきらずどこでインスタンス変数を初期化したか探す羽目になります。

上記のコードの場合、コントローラ名、インスタンス変数名共に post を使ってるので、Railsに慣れてる方なら「多分before_actionだろうな」とすぐに気が付きますが、以下のように、

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit]
  before_action :set_foo, only: [:show, :edit]
  before_action :set_bar, only: [:show, :edit]

  def show; end

  def edit; end

  # ここに行数が長いコード

  private

  # ここに行数が長いコード

  def set_post
    @post = Post.find params[:id]
  end

  def set_foo
    @foo = # ここに @foo 初期化処理
  end

  def set_bar
    @bar = # ここに @bar 初期化処理
  end
end

こうなってくると、Railsに慣れてる方でも探すのに苦労するようになってきます。

これならまだ set_ のプレフィックスがあるので何とか付いていけますが、例えば check_role みたいなbefore_actionで @role インスタンス変数を初期化していると、本格的にわけわからなくなってきます。(実際にこのような書き方でインスタンス変数探すのに苦労した案件が一部あります。)

解決案1

やはり、アクションで必要なインスタンス変数を初期化するのが王道でしょうか。

class PostsController < ApplicationController
  def show
    @post = get_post(params[:id])
    @foo = get_foo
    @bar = get_bar
  end

  def edit
    @post = get_post(params[:id])
    @foo = get_foo
    @bar = get_bar
  end

  # ここに行数が長いコード

  private

  # ここに行数が長いコード

  def get_post(id)
    Post.find id
  end

  def get_foo
    # ここに foo 取得処理
  end

  def get_bar
    # ここに bar 取得処理
  end
end

解決案2

または、以下のようにhelper_methodにしてしまってもよいと思います。

class PostsController < ApplicationController
  helper_method :post
  helper_method :foo
  helper_method :bar

  def show
  end

  def edit
  end

  # ここに行数が長いコード

  private

  # ここに行数が長いコード

  def post
    @post ||= Post.find params[:id]
  end

  def foo
    # ここに foo 取得処理
  end

  def bar
    # ここに bar 取得処理
  end
end

そしてview側では インスタンス変数ではなくヘルパーメソッド を呼び出します。

views/posts/show.html.erb
<p><%= post.body %></p>

<p><%= foo %></p>

<p><%= bar %></p>

これだと「before_action使ってる時とあまり変わらないじゃないか」と思う方もいるかもしれませんが、
helper_method はviewから呼び出すことが前提のメソッドなので、インスタンス変数初期化以外の用途でも使う before_action よりは「あ、これview側で使うんだな」ということがぱっと見で伝わります。

また、helper_method を使った場合、view側がインスタンス変数に依存しなくなるので、例えば @pest のようなtypoをやらかしてもその場でエラーを吐くのですぐに気が付けます。

まとめ

システムの規模が大きくなるとbefore_actionで初期化したインスタンス変数を探すのがしんどくなるのでやめよう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpecデータリクエストを削減してテスト実行時間を削減した話

はじめに

私はエンジニアインターンで主にサーバーサイドの開発を担当させていただいています。

機能実装の際はRSpecでテストケースを書いてから実装していて、コミットする前に全体のテストを通してバグを見つけているのですが

実行時間が長い!

ということで、RSpecのリファクタリングを行いテストの実行時間を削減して見ましたので、一部紹介したいと思います。

1. 従来のRSpecでのデータリクエスト状況

現状、RSpecでは以下のように、letを使用してデータの作成を行っています。

require 'spec_helper'

describe User do
  let!(:user) { FactoryBot.create(:user) }

  it 'anything' do
    # userのテスト
  end

  it 'anything' do
    # userのテスト
  end

  it 'anything' do
    # userのテスト
  end
end

これではexampleが3回行われる度にUserモデルが作成されてしまう。
これをどうにか1回のレコード作成に留められないか...

2. gem "test-prof"の導入

そこでtest-profを導入します。

Gemfile
gem "test-prof"
bundle install

変更後テストケース

require 'spec_helper'

describe User do
  let_it_be(:user) { FactoryBot.create(:user) }

  it 'anything' do
    # userのテスト
  end

  it 'anything' do
    # userのテスト
  end

  it 'anything' do
    # userのテスト
  end
end

このようにtest-profはlet_it_beというヘルパーメソッドを提供してくれるgemになります。

そうするとRailsのトランザクションテスト機能を利用してレコードを最初に1回だけ作成して、テストが終了したら該当データを削除してくれるようになります。

3. ベンチマーク

変更したテストファイル数は全体の3割ほどですが、約1分のテスト実行時間の削減に繋がりました。

let_it_be使用前

スクリーンショット 2021-03-12 18.12.38.png

let_it_be使用後

スクリーンショット 2021-03-12 18.46.49.png

おわりに

画期的にテストの実行時間を削減できた訳ではありませんが、長期的に見て効果のある対処法なのではないかと思います。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル10章まとめ

10.1 ユーザーを更新する

ユーザーを更新する行為は、新規ユーザーの作成に似ている。

アクション リクエストに応答
新規作成 new POSTに対してcreate
更新 edit PATCHに対してupdate

10.1.1編集フォーム

まずやるべきは、Usersコントローラーにeditアクションを追加し、それに対応するeditビューを実装。
ルーティングはresources :usersのおかげで有効になっている。

Railsチュートリアル 表7.1を確認すると、ユーザー編集ページのURLは/users/1/editとなっている。
ここで、ユーザーidを取得するために用いるのが、params[:id]
paramとは、Railsで送られてきた値を取得するメソッド

app/controllers/users_controller.rb
  def edit
    @user = User.find(params[:id])
  end

ユーザーのeditアクション

app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, local: true) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Save changes", class: "btn btn-primary" %>
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="https://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

ユーザーのeditビュー
ここでは、formの部分で、大部分がユーザー作成画面とかぶっているので、パーシャルでまとめるのが良い。

また、@userインスタンス変数を使って、idを取得し編集ページを表示している。

HTML
<form accept-charset="UTF-8" action="/users/1" class="edit_user"
      id="edit_user_1" method="post">
  <input name="_method" type="hidden" value="patch" />
  .
  .
  .
</form>

WebブラウザではそのままではPATCHリクエストを送信できない。
そこで以下のコードで「偽装」している。

<input name="_method" type="hidden" value="patch" />

リスト 10.2のform_with(@user)のコードは、リスト 7.15のコードと完全に同じである。
ここでは、Active Recordnew_record論理値メソッドでPOSTリクエストとPATCHリクエストを区別している。

$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false

true→POST
false→PATCH

最後に、ナビゲーションのリンクを実装

<%= link_to "Settings", edit_user_path(current_user) %>

edit_user_pathもUsersリソースのおかげで使用可能。
current_userはヘルパーメソッド(9章で実装)

10.1.2 編集の失敗

ゴールはupdateメソッドの実装。

app/controllers/users_controller.rb
  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      # 更新に成功した場合を扱う。
    else
      render 'edit'
    end
  end

抑えておきたいのは、updateの呼び出しにuser_paramsを使っている点。
これはStrong Parametersというテクニック

user_paramsメソッドはx Usersコントローラー内部でのみ実行され、Web経由で外部ユーザーに晒される心配はなし。属性の許可を制限することで管理者権限を乗っ取られることを防ぐ。
(詳しくは7.3.2参照)

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end

10.1.3 編集失敗時のテスト

統合テストを行う

$ rails generate integration_test users_edit
      invoke  test_unit
      create    test/integration/users_edit_test.rb

コマンドで統合テストを生成

まずは、編集失敗時のテストから。
流れとしては、編集ページにアクセス→editビューが描画されるか確認→無効な情報を送信→再度editビューが描画される

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "",
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }

    assert_template 'users/edit'
  end
end

ここでPATCHリクエストを送るためにpatchメソッドを使用している。
これはget,postメソッド等と同様に、HTTPリクエストを送るためのもの。
(HTTPリクエストとは、クライアント側からWebサーバーにリクエストする際送信するもの)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveModelに対してエラーメッセージ日本語化 ja.yml書き方

投稿記事にタグ付けをするため、1つのフォーム送信で複数のモデルを更新できるというFormオブジェクトを使用しました。

app/models/posts_tag.rb
class PostsTag
  include ActiveModel::Model
  attr_accessor :title,:text,:answer,:user_id,:image, :name

  with_options presence: true do
    validates :title
    validates :text
    validates :name
  end

  def save
    post = Post.create(title: title,text: text,answer: answer,user_id: user_id,image: image)
    tag = Tag.where(name: name).first_or_initialize
    tag.save

    PostTagRelation.create(post_id: post.id, tag_id: tag.id)
  end
end

そして投稿時にエラーが発生した際に日本語でエラーメッセージが表示されるようにja.ymlを作成したのですが、英語から日本語に変換してくれませんでした。><
↓はダメだった書き方です。

config/locales/ja.yml
ja:
 activerecord:
   attributes:
     user:
       nickname: ニックネーム
     post:
       title: 投稿タイトル
       text: 投稿内容
       image: 画像
       name: タグ名

そこから下記のように修正したところ日本語に変換してくれました!

config/locales/ja.yml
ja:
 activerecord:
   attributes:
     user:
       nickname: ニックネーム
 activemodel:
  attributes:
   posts_tag:
       title: 投稿タイトル
       text: 投稿内容
       image: 画像
       name: タグ名

activerecordとactivemodelの違いも分かっていないプログラミング初学者ですが、
とても良い経験となりました。

おわり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails ローカル環境構築

railsの課題環境構築の手順

この記事がすごくわかりやすかった!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでとにかくDBをリセットしたい場合(メモ)

migrationファイル内を変更している時、いろいろやりすぎて一度DB関連をすべてリセットしたい時のコマンド

rails db:migrate:reset

このコマンドを実行することでデータベースを作り直すことができる。

(流れ・動き)

①データベースを削除

②データベース作成

③マイグレーション実行

この流れで実行されている。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PusherとRailsでprivate channelの通知を使ってチャットを作ってみた

概要

Railsでチャットを作るには、ActionCableが第1候補です。しかし、スケールしたときに、ActionCableを自前のサーバーで面倒見るのはちょっと心配。

ということで、外部サービスいいのないかなーと思って調べたところ、Pusherにゆきつきました。

昔からあるサービスです。しかし、公式でRubyの事例があるにもかかわらず、ネット上でRailsの事例をみません。private channelを使った事例は特に。

なお、Laravelでは事例をよく見ますし、すでに語り尽くされたのかも知れません。

が、僕は初めてだったので、改めてRailsで作りにはどうすればいいかといことで、やったのでその記録をのせます。

使用イメージ

Kapture 2021-03-21 at 18.12.13.gif

使用手順

PUSHERのアカウントを作成

PUSHERの App keysで情報を入寮

.env_sample.env ファイルを .env ファイルにコピーして、PUSHERのApp keys の情報を入力する。

インストール

rails db:migrate
rails db:seed
rails s

以下にアクセスする
http://localhost:3000

ログインする。ブラウザを2つ開いて、それぞれ違うユーザーでログインして下さい。

ついになるようにチャットの相手を選んで下さい。(上述の動画参照のこと)

環境

  • Rubyは 3.0.0
  • dbは sqlite3
  • Rails は 6.1

コードはこちら

Pusherを使うために追加したコードに集中して読みたい場合は以下のPRを参照下さい。

説明

  • 擬似ログインを作る
  • dot-envで設定を管理できるようにする
  • Pusherにsign inして環境変数をセットする
  • Pusherのprivateチャネルをsubscribeする
  • チャットをできるようにする

擬似ログインを作る

本質では無いですが、ログインできた方がイメージが作りやすいように、擬似ログインを作りました。

特定のユーザーのuser_idをsessionに login_user_idとして保存します。

それにより、 current_user authentificate_user! のおなじみのメソッドを定義して参照できるようにしました。

なので、deviseを使っているユーザであればとくやらないでもよいかと。

なお、作ってみるとこれ、以外と使い出があって個人的には満足しています。いろんなところで応用ききそうなので。

それっぽい、endpointを定義。

routes.rb
  get 'sign_in', to: 'sessions#new'
  post 'sign_in', to: 'sessions#create'
  get 'sign_out', to: 'sessions#destroy'

それっぽいsessions_controllerを定義。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
    @users = User.all
  end

  def create
    @user = User.find(user_params[:id])
    session[:login_user_id] = @user.id
    redirect_to root_path
  end

  def destroy
    session[:login_user_id] = nil
    redirect_to root_path
  end

  private

  def user_params
    params.require(:user).permit(:id)
  end
end

dot-envで設定を管理できるようにする

いつものあれです。dot-envです。とくに説明ないです。リンク貼っておきます。

Pusherにsign inして環境変数をセットする

Pusherです。アカウントを持っていない人はつくってください。 sign upしてください。

App keysのメニューに進んで、下図の情報を .envに転記して下さい。

image.png

(下記は例です・)

PUSHER_APP_ID = 1234567
PUSHER_KEY = 77777777777777777777
PUSHER_SECRET = 77777777777777777777
PUSHER_CLUSTER = ap3

Pusherのprivateチャネルをsubscribeする

Pusher の Private channelstoAuthenticating usersが、非常に参考になります。これがあればOK。

該当のコードを下記にはります。

app/controllers/pusher_controller.rb
# frozen_string_literal: true

class PusherController < ApplicationController
  before_action :authenticate_user!

  def auth
    if current_user
      response = Pusher.authenticate(private_channel_name(current_user.id), params[:socket_id], {
                                       user_id: current_user.id # => required
                                     })
      render json: response
    else
      render text: 'Forbidden', status: '403'
    end
  end
end

特に重要なのは、 private_channel_name(current_user.id) private channel。
ここでユーザー毎のprivateチャネルを用意します。

ここで用意したチャネルはprivateになります。他のユーザーはsubscribeできません。

app/controllers/application_controller.rb
  def private_channel_name(user_id)
    "private-channel_user_id_#{user_id}"
  end

privateチャネルの subscribeは下記です。 740c9cda57f8f6c66b27' ここには各自の app_key を入力して下さい。(secret_keyではないので間違えないように!)

'X-CSRF-Token': "<%= form_authenticity_token %>" はCSRF tokenが入ります。

app/views/chats/index.html.erb
    let pusher = new Pusher('<%= ENV['PUSHER_KEY'] %>', {
        authEndpoint: '/pusher/auth,
        cluster: 'ap3',
        encrypted: true,
        auth: {
            headers: {
                'X-CSRF-Token': "<%= form_authenticity_token %>"
            }
        }
    });
    let channel = pusher.subscribe("<%= @private_channel_name %>");

チャットをできるようにする

チャットの送信は下記です。普通にpostのフォームです。 local: false, remote: true が特徴です。

app/views/chats/index.html.erb
  <%= form_with model: @chat, url: chats_path(with_user_id: @received_user.id), local: false, remote: true, id: 'chat-form' do |f| %>
    <%= f.text_field :message, autocomplete: 'off' %>
  <% end %>

上記のpostを下記のcreateで処理します。

app/controllers/chats_controller.rb
  def create
    @user = current_user
    @received_user = User.find(params[:with_user_id])
    @chat = Chat.new(user: @user, received_user: @received_user, message: chat_params[:message])
    if @chat.save!
      data = {
        id: @chat.id,
        message: @chat.message,
        user_name: @chat.user.name,
        user_id: @chat.user.id,
        created_at: @chat.created_at.to_s
      }
      Pusher.trigger_batch(
        [
          { channel: private_channel_name(@user.id), name: event_name(@received_user.id), data: data },
          { channel: private_channel_name(@received_user.id), name: event_name(@user.id), data: data }
        ]
      )
    else
      # なにかエラー処理
    end
  end

重要なのは下記です。ここで、チャット送信者と受信者の両方のprivate チャネルへ通知をしています。

app/controllers/chats_controller.rb
 Pusher.trigger_batch(
        [
          { channel: private_channel_name(@user.id), name: event_name(@received_user.id), data: data },
          { channel: private_channel_name(@received_user.id), name: event_name(@user.id), data: data }
        ]
      )

上記を通知を下記で受信して、画面の描画を更新します。

event_nameの箇所には、やりとりしているユーザー独自のイベント名が入ります。

app/views/chats/index.html.erb
    channel.bind("<%= @event_name %>", function (data) {
        let chat_message = data.message;
        let chat_created_at = data.created_at;
        let chat_user_name = data.user_name;
        let chat_user_id = data.user_id;
        let new_content = document.createElement('div');
        new_content.innerHTML = `
  <div>
    <span>「送信者」id=${chat_user_id} の ${chat_user_name}</span>
    <span>「送信日時」${chat_created_at}</span>
    <span>「メッセージ」${chat_message}</span>
  </div>
`
        let post_section_div = document.getElementById('post_section');
        post_section_div.appendChild(new_content);
        let post_input = document.getElementById('chat_message');
        post_input.value = '';
    });

event_nameはこんな感じ。user_idにはやりとりしている相手のuser_idがはいります。

app/controllers/application_controller.rb
  def event_name(user_id)
    "new_post_with_user_id_#{user_id}"
  end

チャット末尾に追加する。

app/views/chats/index.html.erb
        let post_section_div = document.getElementById('post_section');
        post_section_div.appendChild(new_content);

所感

使いやすいです。かなり簡単です。

ActionCableを使う時は Redisとセットだったりすることを考えると、Pusherの有料プランををつかってもいいのかなあーと思いました。

サーバーの心配せずに夜ぐっすり眠れそうなので。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails - テストコード基礎(Rspec・FactoryBot)

テストコードの実装手順

Rspec・FactoryBotを使った基礎的なテストコード実装についてアウトプットもかねて自分用にまとめます。(今回はuserモデルの単体テストコード)


まずは実装の流れになります。

1.Gemインストール
2.Rspecの設定
3.FactoryBotの設定
4.テストコード記述ファイル作成
5.テストコード記述
6.テスト実行


1.Gemインストール

Gemfile
group :development, :test do
  gem 'rspec-rails', '~> 4.0.0' #バージョン指定
  gem 'factory_bot_rails'
end
ターミナル
bundle install

2.Rspecの設定

Rspecのインストール、ディレクトリ・ファイルを作成する

ターミナル
rails g rspec:install

テストコードの結果をターミナル上に可視化する

.rspec
--require spec_helper
--format documentation #追加

3.FactoryBotの設定

specディレクトリ内にfactoriesディレクトリ、user.rbを作成後

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    nickname              {'test'}
    email                 {'test@com'}
    password              {'111111'}
    password_confirmation {password}
  end
end

4.テストコード記述ファイル作成

ターミナル
rails g rspec:model user

5.テストコード記述

spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
  before do
    @user = FactoryBot.build(:user) #インスタンス生成
  end

  describe 'ユーザー新規登録' do 
    it 'nicknameが空では登録できない' do
      @user.nickname = ''
      @user.valid?
      expect(@user.errors.full_messages).to include "Nickname can't be blank"
    end
  end
end

6.テスト実行

ターミナル
bundle exec rspec spec/models/user_spec.rb 

おまけ

エラーメッセージの日本語化

以下の手順でエラーメッセージを日本語にすることもできる。

1.Gemインストール

config/application.rb
module Pictweet
 class Application < Rails::Application
   # 日本語の言語設定
   config.i18n.default_locale = :ja #追加
 end
end
Gemfile
gem 'rails-i18n'
ターミナル
bundle install

2.各種ファイルの作成、記述

  • config/localesディレクトリにdevise.ja.ymlファイルを作成
  • devise-i18nから記述をコピーして貼り付け
config/locales/devise.ja.yml
ja:
  activerecord:
    attributes:
      user:
        confirmation_sent_at: パスワード確認送信時刻
        confirmation_token: パスワード確認用トークン
        confirmed_at: パスワード確認時刻
        created_at: 作成日
        current_password: 現在のパスワード
        current_sign_in_at: 現在のログイン時刻
        current_sign_in_ip: 現在のログインIPアドレス
        email: Eメール
〜〜〜〜〜〜〜〜〜〜 以下略 〜〜〜〜〜〜〜〜〜〜〜〜〜〜
  • config/localesディレクトリにja.ymlファイルを作成
config/locales/ja.yml
ja:
 activerecord:
   attributes:
     user:
       nickname: ニックネーム
     tweet:
       text: テキスト
       image: 画像

これで日本語化は完了です、あとはテストコードのエラーメッセージ部分を書き換えればテストも通ります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】 dependent: :destroy とは

どんな機能なの?

結論から言うと、dependent: :destroyを使いことによって
親モデルのレコードを削除する際に、それに紐づく子モデルのレコードも同時に削除されます。

具体的な使い方

dependent: :destroyはモデルのアソシエーションで使います。
例えば下記のようにユーザーとマイクロポストのアソシエーションを定義します。

user.rb
has_many :mictroposts

これだけではユーザーが退会した時、そのユーザーのマイクロポストは残ってしまいます。
そのような場合に下記のようにdependent: :destroyを追加します。

user.rb
has_many :mictroposts, dependent: :destroy

こうすることでユーザー退会時には、そのユーザーのマイクロポストも同時に削除されます。

最後に

この機能は他に、いいね機能(いいねした投稿、された投稿が削除された場合、LikeModelのレコードも削除する。)やフォロー機能(フォローしたユーザー、されたユーザーが削除された場合、RelationshipModelのレコードも削除する。)など、アソシエーションをいじる場面でよく出てくるので必ず覚えておきたいですね!

また基本、応用関係なくなるほどと思ったことについて日記感覚で投稿していきます!
ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyとRailsの個人的によく使うメソッドをまとめてみた

今回はタイトルにもある通り、個人的によく使うメソッドをまとめてみました。
初学者なので間違いがあれば指摘していただけると嬉しいです。

そもそもメソッドとは?

メソッド = 処理。

  • 自分で作るメソッド
  • Rubyがあらかじめ用意してくれているメソッド

のふたつがある。
今回はRubyがあらかじめ用意してくれている便利メソッドを紹介します。

putsメソッド

ターミナルに値を出力するメソッド。putsの後に値を記述することで出力できる。

puts "Helloworld"

=> Helloworld

getsメソッド

ターミナルに値の入力機能を起動するメソッド。入力された値は文字列としてプログラムに渡される。
また文字列として扱う際、getsで取得した値の後は、出力の際に改行され、gets.to_iで数値として扱う場合は改行されない。

name = gets
puts "こんにちわ、" + name + "さん!"

=> 好きな文字列を入力する(今回は太郎)

=> こんにちわ、太郎
  さん!

上記の通り、このままだと改行がされてしまうのでそんな時はchompメソッドを合わせて使うと良い。

chompメソッド

文字列の末尾に存在する改行を取り除くメソッド。

name = gets.chomp
puts "こんにちわ、" + name + "さん!"

=> 好きな文字列を入力する(今回も太郎)

=> こんにちは、太郎さん!

となって改行されるのを防いでくれる。

lengthメソッド(文字列)

文字列の文字数を数えるメソッド。その際、半角スペースも文字数として数えられる。

出力する際は数値として出力される。

a = "Helloworld".length

puts a

=> 10

ちなみにこんな感じで配列にくっつければ要素数を数えてくれる。

a = ["Hello","World"].length

puts a

=> 2

to_sメソッド

整数 → 文字列に変換するメソッド。
通常、整数(Integer)型と文字列(String)型による足し算はできないが、こうして型を換えることで可能になる。

puts 20.to_s + "歳"

=> 20歳

ただ、下記のように文字列に対して掛け算をすることは整数(Integer)型に変換しなくても可能。

puts "歳" * 10

=> 歳歳歳歳歳歳歳歳歳歳歳

to_iメソッド

文字列(String) → 整数(Integer)に変換するメソッド。

a = "10".to_i

puts 20 + a

=> 30 

ちなみに文字列をto_iで変換し、整数型と四則演算すると、全て0として変換されるらしい。
冷静に考えたら当たり前なのだが、最近まで知らなかった。

理想↓↓

a = "歳".to_i

puts 20 + a

=> 20歳

現実↓↓

a = "歳".to_i

puts 20 + a

=> 20

getsメソッドと合わせて使うと応用が効いて便利。
ちなみにgetsのsはstring(文字列)型のsらしいです。
(複数形だと思ってたなんて口が裂けても言えない....)

input = gets.to_i

puts 100 + input

=> 好きな数値を入力(今回は100を入力する)
   200

その他類似の変換系メソッド

個人的にはまだ活用できていないが、使い方を理解できると便利であろう変換系のメソッド。

メソッド   効果
to_f Floatクラス(浮動小数点数)に変換
to_sym シンボルに変換
to_h ハッシュオブジェクトに変換
to_a 配列オブジェクトに変換

requireメソッド

外部ファイルを読み込むためのメソッド。require "外部ファイル名"で読み込める。
ただしカレントディレクトリから読み込む場合はrequire "./外部ファイル名"で読み込む。

日付や時間を取得する際に便利なメソッド

Dateクラス

Rubyの標準ライブラリの機能で日付を扱うためのクラスのこと。
require "date"という一文をコード内に記述することで使用できる。

todayメソッド

本日の日付を取得するメソッド。
例)Date.today 今日の日付を取得する

wdayメソッド

0~6の数値で曜日を取得するメソッド。
0:日曜日、1:月曜日、2:火曜日、3:水曜日、4:木曜日、5:金曜日、6:土曜日を表す。

例)Date.today.wday 今日の曜日を数値で取得する

strftimeメソッド

日時データを指定したフォーマットで文字列に変換するメソッド。

例)message.created_at.strftime("%Y年%m月%d日 %H時%M分") 保存された日時を文字列で取得する

eachメソッド

配列や範囲オブジェクトなどで用意されているメソッドであり、オブジェクトに含まれている要素を順に取り出し、変数に格納します。

オブジェクト.each do |変数|
  実行する処理1
  実行する処理2
end

↓↓↓↓

@posts = Post.all だとここでは認識しておいてください。

 <% @posts.each do |post| %>
   タイトル:<% post.title %>
   投稿者:<% post.user %>
   投稿内容:<% post.product %>
<% end %>

each_with_indexメソッド

each(要素の繰り返し処理)と同時に、その要素が何番目に処理されたかを表すメソッド。デフォルトだと処理番号は0から割り当てられる。
僕はランキング機能を作成するさいに用いました。

オブジェクト.each.with_index do |変数1,変数2|
  実行する処理1
  実行する処理2
end

↓↓↓↓

 <% @posts.each.with_index(1) do |post, i| %>
   第<%= i %>位
   <% post.title %>
<% end %>

@postsの中身と指定した数字(1)が |post, i|にそれぞれ格納され、「第◯位」と「投稿のタイトル」に割り振られる。

include?メソッド

指定した要素が配列や文字列内に含まれているかを判定するメソッド。

number = [100, 200, 300 , 400 , 500]

puts number.include?(200)

=> true

splitメソッド

指定した区切り文字で対象となる文字列を分割して配列にする。

fruit = "apple,strawberry,grape,banana"

puts fruit.split(',')

=> ["apple", "strawberry", "grape", "banana"]

sliceメソッド

配列や文字列から指定した要素を取り出すことができるメソッド。
文字列の要素を指定する際は数字を用い、先頭の文字列は0から指定する。また第二引数として、取り出す要素の数も指定したい場合は指定することもできる。

number = [100, 200, 300 , 400 , 500]

puts number.slice(3,2)

=> [400 , 500]

エクスクラメーションマーク(!)

末尾につけることにより破壊的メソッドになり、もとの配列や文字列を変化させるメソッド。

scanメソッド

対象の要素から引数で指定した文字列を数え、配列として返すメソッド。

puts "ワン!ワンッ!ワンワンッ!".scan("ワ")

=> [ワ, ワ, ワ, ワ]

lengthと組み合わせると含まれている"ワ"の数で返ってくる。

puts "ワン!ワンッ!ワンワン!".scan("ワ").length

=> 4

ただ、下記にcountというメソッドもあるのでそっちの方が使いやすい。

countメソッド

特定の文字列の中に指定した文字列がいくつ含まれているかを数えたり、配列の要素数を数えるメソッド。

puts "ワン!ワンッ!ワンワン!".count("ワ")

=> 4

firstメソッド

配列から最初の要素を取得するメソッド。
引数がある場合は配列の最初の要素からその引数分の要素を配列で取得する

name = ["太朗", "花子", "二郎"]

puts name.first(2)

=> ["太朗", "花子"]

lastメソッド

配列から最後の要素を取得するメソッド。
引数がある場合は配列の最後の要素からその引数分の要素を配列で取得する

name = ["太朗", "花子", "二郎"]

puts name.last(2)

=> ["花子", "二郎"]

sortメソッド

配列の要素を一定の規則(アルファベット、数値の降順、昇順など)で並び替えるメソッド。

fruit = ["apple", "strawberry", "grape", "banana"]

puts fruit.sort

=>  ["apple", "banana", "grape", "strawberry"]

joinメソッド

配列内の要素と要素を連結させるメソッド。
引数を渡すことで区切ることもできる。

fruit = ["apple", "strawberry", "grape", "banana"]

puts fruit.join(",")

=>  apple,strawberry,grape,banana

pushメソッド

配列の末尾に要素を連結する

fruit = ["apple", "strawberry", "grape", "banana"]

puts fruit.push("orange")

=> ["apple", "strawberry", "grape", "banana","orange"]

unshiftメソッド

配列の先頭に要素を追加する。

fruit = ["apple", "strawberry", "grape", "banana"]

puts fruit.unshift("orange")

=> ["orange", "apple", "strawberry", "grape", "banana"]

shiftメソッド

配列の先頭の要素を削除する。または配列の先頭の要素を取り出す。

fruit = ["apple", "strawberry", "grape", "banana"]

puts fruit.shift

=> ["Apple"]

pluckメソッド

引数に指定したカラムの配列を返す(Railsのメソッド)

#Fruitモデルのnameカラムに "Apple" "grape" "banana" が入っている場合
=> Fruit.pluck(:name)
=> ["Apple","grape","banana"]

最後に

まだまだ足りないメソッドだらけで、適切に使えているかどうかも定かではありませんが、使える技が増えればそれだけ自分の中のアイデアを形にすることができると思います。
また、思い付いたらこれから追加していく予定です。
誰かの手助けになれればいいなと思います。
間違いや指摘があればコメントに残していただけると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails6】多対多のアソシエーションを利用したグループ参加機能の実装

多対多のアソシエーションを利用してグループ参加機能を実装する方法を紹介します。
目的

  • 前提条件
  • グループ参加機能の仕様
  • 動作確認
  • 実装手順1:モデルの作成
  • 実装手順2:コントローラーの作成
  • 実装手順3:ルーティングの設定
  • 実装手順4:ビューの作成

前提条件

Rails 6.0.3.5

グループ参加機能の仕様

承認制ではなく、グループに参加するボタンをクリックすると、データベースにユーザーIDとグループIDに組み合わせが保存されて参加できる簡単なものになります。

動作確認

  • 「Join this group」をクリック
  • Are you sure to join this group?」というメッセージが出る
  • OKをクリックするとグループのトップページに移動する
  • 「Notice: joined the group!!」のフラッシュメッセージが出る。

これでグループへの参加が完了です。
Image from Gyazo
それでは説明していきます。
グループ参加機能の実装に関係する記述以外は省略しているのでご承知おきください。

実装手順1:モデルの作成

用意するモデル(テーブル)はuser, group, user_groupの3つです。

app/models/user.rb
class User < ApplicationRecord
  has_many :user_groups
  has_many :groups, through: :user_groups
end
app/models/group.rb
class Group < ApplicationRecord
  has_many :user_groups
  has_many :users, through: :user_groups
end
app/models/user_group.rb
class UserGroup < ApplicationRecord
  belongs_to :user
  belongs_to :group

  validates :user_id, uniqueness: { scope: :group_id }
end

実装手順2:コントローラーの作成

groups_controlleruser_groups_controllerを以下のように記述します。グループ参加機能の実装には、users_controllerへの記述は特にありません。

app/controllers/groups_controller.rb
class GroupsController < ApplicationController
  def show
    @group = Group.find(params[:id])
    # UserGroupテーブルからログインユーザーと、詳細を表示しているグループの組み合わせを探します。
    # 組み合わせがなければnilを返します。
    @userGroup = UserGroup.find_by(user_id: current_user.id, group_id: params[:id])
  end
app/controllers/user_groups_controller.rb
class UserGroupsController < ApplicationController
  def create
    @userGroup = UserGroup.new(user_id: current_user.id, group_id: params[:group_id])
    if @userGroup.save
      # グループ参加後のリダイレクト先を指定
      redirect_to XXXX_path(@userGroup.group_id), notice: 'joined the group!!'
    end
  end
end

実装手順3:ルーティングの設定

user_groups_controller.rbparams[:group_id]をを使用するアクションを定義しているため、user_groupsコントローラーgroupsコントローラーにネストさせます。

config/routes.rb
Rails.application.routes.draw do
  resources :groups, only: :show do
    resources :user_groups, only: :create
  end
end

実装手順4:ビューの作成

ボタンの部分の記述だけ掲載します。
条件分岐で@userGroupに値が入っていれば、グループに参加しているのでグループメンバーしか見られない情報を表示し、値が入っていなければで「Join the group」ボタンを表示します。

app/views/groups/show.html.erb
<% if @userGroup.present? %>
  <div class="form-group">
    <div class="schedule-item">
      <div class="schedule-item-header text-left">
        <p class="bold">Task</p>
      </div>
      <div class="schedule-item-body text-left">
        <p><%= @group.task %></p>
      </div>
    </div>
  </div>
<% else %>
  <div class="actions">
    <%# user_groupsコントローラーのcreateアクションを実行するリンク %>
    <%= link_to 'Join this group',  group_user_groups_path(@group.id), method: :post, data: { confirm: 'Are you sure to join this group?'}, class:"sign-up-btn" %>
  </div>
<% end %>

これでグループ参加機能の実装完了です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】古いデータベースが影響してエラーが出た話

概要

以前作成したRailsアプリを削除して、再び同じ名前で同様のアプリ作成しました。
そしてrails db:migrate実行後、ActiveRecord::PendingMigrationErrorが発生した。

結論から言うと、過去アプリのデータベースが残っており
作成したアプリ上のdb/migrateディレクトリ内のファイルをコマンドdb:migrateにて反映させようとした際、ファイルのバージョンが異なるためエラーが発生しました。
2021-03-17 13.20.47.png

開発環境

・データベース:PostgreSQL
・railsのバージョン:6.1.3
・PC:macbook pro

エラー発生から解決までの流れ

  1. 元々あったrailsアプリを削除する。(rails db:create,rails db migrate実行済み)
  2. 元々あったrailsアプリと同じ名前のrailsアプリを再び作成する。(rails db:create実行済み)
  3. rails db:migrateを実行後、rails sにてサーバを立ち上げてブラウザを見ると、ActiveRecord::PendingMigrationErrorが発生する。
  4. rails db:versionを実行すると、terminal上のファイルとアプリmigrationファイル(db/migrate)のバージョンが異なる。
  5. rails db:resetを実行してrails db:migrateを行うと、問題なくアプリケーションを実行できる。
    また、rails db:versionにてファイルを確認すると、terminal上とrailsアプリのmigrationファイルが一致していることが確認できた。
  6. rails db:migrateを行ってアプリを起動してみると、問題なく動作した。 スクリーンショット 2021-03-17 12.22.31.png

エラーが発生した原因

原因は私の理解不足によるもので、アプリファイルを削除すると一緒にデータベースも消すことができると思っていたためです。
(アプリファイルとデータベースは、同一の場所にあると思っていた)
実際は、下記ディレクトリにデータベースが保存されています。
また、データベース削除とversion確認コマンドは、下記になります。

PostgreSQL:… /usr/local/var/postgres 内
MySQL:… /usr/local/var/mysql 内

データベース削除:rails db:reset (データベース内の情報は消えるので注意!)
データベースversion確認:rails db:version

最後に

正直なところ、データベースについてまだまだわかっていない点がたくさんあります。
もし間違っている点等がある場合、ご指摘頂けると大変ありがたいです。
最後まで読んで頂きありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テストコード その2

テストコードを実装するための準備

復習として作成しました。

RspecのGemを導入する。

Gemfileのgroup :development, :test do~endの間にgem 'rspec-rails', '~> 4.0.0'と記述する。
このように記述するとGemの動作に制限を持たせることができる。
bundle install →rails g rspec:installの順でファイルを作成する。
.rspecファイルを開き、--format documentationと記述する。
こう記述することでテストコードの結果を可視化する。
rails g rspec:model テストしたいモデル名でモデルのテストファイルを作る。
これで準備OK。

テストコードの雛形

describe
テストコードのグループ分けをする。ここでは主にどの機能に対してテストコードを行うかをグループ分けする。
例:Userモデルの新規登録機能についてテストコードを行うとなると以下のようになる。

rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe '新規登録' do
    # 新規登録について記述する ​
 ​end
end

it
itはdescrideでグループ分けした機能でどのような状況のテストコードを行うか記述する。
上の# 新規登録について記述する がitに当たる。
形を整えるとこんな感じになる。

rb
it "新規登録ができる" do
  # 登録ができるテストコードを記述する
end

example
itで小分けしたグループのことまたは、itに書かれた内容のことを差す。

テストコードを実行するときは以下のコマンドをターミナルに打つ。

ターミナル
bundle exec rspec spec/models/user_spec.rb 

bundle execでGemの依存関係を整理してくれる。多くのGemは他のGemと依存関係が成り立っているため整理する必要がある。
rspecでspecディレクトリ以下に書かれたテストコードを実行している。

結果が緑色だったら成功となる。

テストコードに使われるメソッド

vaild?メソッド
通常validateが働くときは実際に入力して実行したときである。
なので任意のタイミングでバリデーションを実行させ、確認する。
エラーがあればfalseをなければtrueを返してくれる。

expectation
テストコードを実行した結果が想定通りかどうか確認する。引数には検証で得られた実際の挙動を指定する。

matcher
expectの引数と想定した挙動が一致しているか確認する。どのような挙動を想定しているか記述する。
■代表的なmatcher
・include
expectの引数にincludeの引数が含まれていることを確認する。
実際の検証結果がincludeの引数と同じであることとみる。
・eq
expectの引数とeqの引数が等しいことを確認する
例えばexcept(1+1).to eq(2)という感じ。

valid?を実行し、falseが返ってきたときエラーの内容を確認するメソッド

errors
インスタンスにエラーを示す情報がある場合、その内容を返すメソッド
full_messages
エラー内容からエラーメッセージを配列として取り出し、エラーメッセージを表示するメソッド

以上を踏まえて、Nameのvaild?がfalseだったときに想定することが起きるかどうかを確認したいコードは以下の通り

rb
expect(user.erroors.messages).to include("Name can't be blank")

exceptで取り出したエラーメッセージがincludeに含まれたものであればテストは成功という事になる。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

投稿機能 統合テストの実装

今回ポートフォリオ作成中にでたエラーの解決策を備忘録も含め、同じところでつまづかれている方などいましたら、ご参考にして頂けましたら幸いです。

投稿機能では画像つきの機能を取り入れてまして、
でたエラーに関してが、

Capybara::ElementNotFound: Unable to find field "post[images][]" that is not disabled
from /Users/takuya/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/capybara-3.35.3/lib/capybara/node/finders.rb:303:in `block in synced_resolve'

上記のエラーでした。

アウトプットの前に、
rspec
factory.bot
上記は導入済みで、進めさせていただきます。

ちなみにかなり時間を取られました?

投稿機能統合テスト記述

RSpec.describe '写真テキスト投稿', type: :system do
  before do
    @user = FactoryBot.create(:user)
    @post_name = Faker::Lorem.sentence
  end
  context '写真テキスト投稿ができるとき'do
    it 'ログインしたユーザーは新規投稿できる' do
      # トップページに移動する
      visit root_path
      # トップページにログインページへ遷移するボタンがあることを確認する
      expect(page).to have_content('LOGIN')
      # ログインページへ遷移する
      visit new_user_session_path
      # 正しいユーザー情報を入力する
      fill_in 'Eメール', with: @user.email
      fill_in 'パスワード', with: @user.password
      # ログインボタンを押す
      find('input[name="commit"]').click
      # トップページへ遷移することを確認する
      expect(current_path).to eq(root_path)
      # 写真投稿一覧ページに移動する
      visit posts_path
      # 新規投稿ページへのリンクがあることを確認する
      expect(page).to have_content('PUSH 投稿')
      # 投稿ページに移動する
      visit new_post_path
      # フォームに情報を入力する
      fill_in 'post[images][]', with: "public/images/test_image.jpg"
      fill_in 'post[name]', with: @post_name
      # 送信するとPostモデルのカウントが1上がることを確認する
      expect{
        find('input[name="commit"]').click
      }.to change { Post.count }.by(1)
      # 写真投稿一覧ページに移動する
      visit posts_path
      # 写真投稿一覧ページには先ほど投稿した内容の写真テキストが存在することを確認する(画像)
      expect(page).to have_selector("img[src$='test_image.png']")
      # 写真投稿一覧ぺージには先ほど投稿した内容の写真テキストが存在することを確認する(テキスト)
      expect(page).to have_content(@post_name)
    end
  end
  context '写真投稿ができないとき'do
    it 'ログインしていないと新規投稿ページに遷移できない' do
      # 写真投稿一覧ページに移動する
      visit posts_path
      # 新規投稿ページへのリンクがない
      expect(page).to have_no_content('nPUSH 投稿')
    end
  end
end

fill_in 'post[images][]', with: "public/images/test_image.jpg"
の上にbinding.pryを入れて、上記コードを入力すると画像が挿入できない状態でした。

要素中にinputタグのname要素を入れ込んでいるのになぜエラーが出るのかと四苦八苦していました。

一度頭を整理して、fill_in意外にも画像挿入する方法はないかと調べたところ、

attach_file

というメソットが引っかかりました。

そこでfill_inを削除してattach_fileメソットの記述を行いました。

変更前

# フォームに情報を入力する
      fill_in 'post[images][]', with: "public/images/test_image.jpg"
      fill_in 'post[name]', with: @post_name

変更後

# フォームに情報を入力する
      attach_file('post[images][]', image_path, make_visible: true)
      fill_in 'post[name]', with: @post_name

上記を記述変更を行い、今すぐにでも実装を行いたかったのですが、焦ってはいけませんでした

変更後の記述の中にimage_pathを代入していることから、
フォームに情報を入力する
の統合テストを行う前に、添付する画像を定義する記述を行いました。
そうしないとなんの画像を指定しているのかわからないためです

そして、

# 添付する画像を定義する
      image_path = Rails.root.join('public/test_image.png')
      # フォームに情報を入力する
      attach_file('post[images][]', image_path, make_visible: true)
      fill_in 'post[name]', with: @post_name

フォーム情報を入力するの前に、画像を定義する記述を行い、無事テストが行える状況になりました。

実装を行うと、問題なく統合テストを行うことができました

完成コード

RSpec.describe '写真テキスト投稿', type: :system do
  before do
    @user = FactoryBot.create(:user)
    @post_name = Faker::Lorem.sentence
  end
  context '写真テキスト投稿ができるとき'do
    it 'ログインしたユーザーは新規投稿できる' do
      # トップページに移動する
      visit root_path
      # トップページにログインページへ遷移するボタンがあることを確認する
      expect(page).to have_content('LOGIN')
      # ログインページへ遷移する
      visit new_user_session_path
      # 正しいユーザー情報を入力する
      fill_in 'Eメール', with: @user.email
      fill_in 'パスワード', with: @user.password
      # ログインボタンを押す
      find('input[name="commit"]').click
      # トップページへ遷移することを確認する
      expect(current_path).to eq(root_path)
      # 写真投稿一覧ページに移動する
      visit posts_path
      # 新規投稿ページへのリンクがあることを確認する
      expect(page).to have_content('PUSH 投稿')
      # 投稿ページに移動する
      visit new_post_path
      # 添付する画像を定義する
      image_path = Rails.root.join('public/test_image.png')
      # フォームに情報を入力する
      attach_file('post[images][]', image_path, make_visible: true)
      fill_in 'post[name]', with: @post_name
      # 送信するとPostモデルのカウントが1上がることを確認する
      expect{
        find('input[name="commit"]').click
      }.to change { Post.count }.by(1)
      # 写真投稿一覧ページに移動する
      visit posts_path
      # 写真投稿一覧ページには先ほど投稿した内容の写真テキストが存在することを確認する(画像)
      expect(page).to have_selector("img[src$='test_image.png']")
      # 写真投稿一覧ぺージには先ほど投稿した内容の写真テキストが存在することを確認する(テキスト)
      expect(page).to have_content(@post_name)
    end
  end
  context '写真投稿ができないとき'do
    it 'ログインしていないと新規投稿ページに遷移できない' do
      # 写真投稿一覧ページに移動する
      visit posts_path
      # 新規投稿ページへのリンクがない
      expect(page).to have_no_content('nPUSH 投稿')
    end
  end
end

画像挿入するだけのことでしたがかなり時間を使ってしまいました。

冷静に頭を整理して、根本のところから他に定義するメソットはないのという発想で成功することができました。

もし似たような内容で詰まっている方などいましたら、ご参考にして頂けましたら幸いです。

宜しくお願いします!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails generate がうまくいかない時

原因
gem のspringがエラーを起こしている。

そもそもgem とは
・Rails4からデフォルトで入ったアプリケーションプリローダー。
・一部のタスクを前もって実行しておき、その後に必要なタスク時間を短縮する。
・アプリケーションをバックグラウンドで実行し続けてくれる。
・アプリーケーションコードを編集したら内部のリロードを自動的に実行する。

主に開発をする上で開発者に余計なタイムロスを発生させないようにするためのパッケージ。

解決方法
バックグラウンド側で動いているspringを止めてしまう。
・spring stop

これでOK!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails urlパス変更

コントローラー名とは違う命名でルーティングを指定したい!
そんなことが起きた時に使えるコマンドを紹介します。

resourcesとネスト

Railsのルーティング記法の基本は、複数形のresourcesメソッドと単数形のresourceメソッドです。また、Railsのルーティングにはネストを含む多くのオプションがあり、自由度が飛躍的に高まっています。

始めに

スクリーンショット 2021-03-21 15.00.37.png

resources :notifications, only: [:show]

notificationコントローラーのshowアクションにと、ごく普通の記述です。

/notifications/:idの部分を違う名前に変更したいと思い。

pathオプションを使用する。

resources :notifications, only: [:show], :path => "articles" 

スクリーンショット 2021-03-21 15.06.23.png

おわりに

些細なことでも疑問に思った点
解決した点など更新して行きます?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

再新規登録はできません 勝手にredirectされます

初めてrailsでポートフォリを作りをしています。
index画面と新規登録画面を作って
deviseをインストールして
画面表示もindex、新規登録画面ともに表示され
新規登録1度行ない、その時入力した情報が
データベースにも保存されていたことを確認しました。

嬉しくなって再度新規登録画面を開け再度、登録ようとしました。
何度も。。。
http://localhost:3000/users/sign_up
を開いても
http://localhost:3000/が開きます。

何度も

エラーは出ていないのでターミナルを見て考えました。

Started GET "/users/sign_up" for ::1 at 2021-03-21 14:07:01 +0900
Processing by Devise::RegistrationsController#new as HTML
  User Load (36.8ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1
Redirected to http://localhost:3000/
GET "/users/sign_up" 

でアクセスしましたが

devise::RegistrationsController

によって
UserがLoadされ

SELECT `users`.* FROM `users` WHERE `users`.`id` = 1(SQL文)

⇨id番号1番のuserなのでログインしているとして

Redirected to http://localhost:3000/

元の画面(localhost:3000/)にリダイレクトされているとのことでした。
無駄なことをしていました。
早くログアウト機能つけよう。残念!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

inquiry

TL;DR

文字列等価のチェックで'hoge' == 'hoge'という書き方以外の方法があります。
Rails AcctiveSupportにはStringオブジェクトの拡張でinquiryというメソッドが定義されています。

word = 'hoge'.inquiry
word       # => 'hoge'
word.hoge? # => true
word.fuga? # => false

どう定義されてる?

active_support/core_ext/string/inquiry.rb で定義されてます。

lib/active_support/core_ext/string/inquiry.rb
require "active_support/string_inquirer"

class String
  def inquiry
    ActiveSupport::StringInquirer.new(self)
  end
end

ではActiveSupport::StringInquirerは何者?

次で定義されてます。

lib/active_support/string_inquirer.rb
module ActiveSupport
  class StringInquirer < String
    ...
    def method_missing(method_name, *arguments)
      if method_name.end_with?("?")
        self == method_name[0..-2]
      else
        super
      end
    end

    def respond_to_missing?(method_name, include_private = false)
      method_name.end_with?("?") || super
    end
  end
end

method_missingの部分がinquiryメソッドの核になってます。
メソッド名の最後に?がついている場合、そのメソッド名の?を除いた部分がインスタンス名と一致するかの真偽値を返します。

ちなみに...

method_missingは呼び出したメソッドが定義されていなかった時に呼び出されるRuby組み込みのメソッドです。一つ目の引数に呼び出しに失敗したメソッド名、その引数が第二引数に渡されます。
つまりこんな感じ

class String
  def method_missing(method_name, *args)
    puts "method named #{method_name} is not defined!"
  end
end

'hoge'.fuga # => method named fuga is not defined!

またrespond_to_missing?も同じくRuby組み込みのメソッドです。method_missingを呼び出すに該当するメソッドがrespond_to?で呼び出された際に応答します。

class String
  def method_missing(method_name, *args)
    puts "method named #{method_name} is not defined!"
  end

  def respond_to_missing?(method_name, include_private)
    method_name.end_with?("?") || super
  end
end

'hoge'.respond_to?(:fuga?) # => true 
'hoge'.respond_to?(:piyo) # => false

終わり

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

scope使い方(Rails)

modelの中にscopeを書く

複数使う場合はスコープで書いた方がコードが読みやすくなりますし、
単体テスト書くことが可能になるため積極的に使った方がいいです。

item.rb
 scope :published, -> { where(private: true) }
users_controller.rb
 def show
    if !@user.me?(current_user)
      @items = @user.items.published
    else
      @items = @user.items
    end
  end

モデルで{ where(private: true) }定義することで
コントローラの記述が少なくなります。

おまけ

user.rb
def me?(user_id)
  id == user_id
end

current_user@userが一致しているか
モデルに書くことで可能になります。

users_controll.rb
if !@user.me?(current_user)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エラーメッセージを自分でカスタマイズする

バリデーションの内容を自分好みに表示させる方法です。備忘録として載せたいと思います。
項目ごとに記述しているのでスマートではないかもしれません!
※現在は勉強段階なので実際の現場でどう使われているかは分かりません。

こんな感じに表示されます↓
スクリーンショット 2021-03-20 22.40.18.png

実装

エラーの記述部分だけ載せたいと思います

モデル

contact.rb
class Contact < ApplicationRecord
  validates :title, presence: { message: "タイトルを入力してください" }
  validates :body, presence: { message: "内容を入力してください" }
  validates :name, presence: { message: "名前を入力してください" }
  validates :email, presence: { message: "メールアドレスを入力してください" }
end

messageに好きなメッセージを設定できます。

コントローラ

※newページのform_withではconfirmアクションに飛ぶように指定しています

contacts_controller.rb
def confirm
    @contact = Contact.new(contact_params)
    @contact.valid?
    if @contact.errors.present?
      return render "new"
    end
  end

保存する時ならエラーの検証が実行されますが、今回は確認画面を挟んでいるためvalid?メソッドを使い自分でトリガーを引いてあげます。(=.errorsでエラー内容を参照できる)
(valid?:エラーの有無を判定するメソッド。正常であればtrueを返す。)

valid?をしないとエラーメッセージが入ってきません(=.errorsが使えない)
rails console(下)で確認してみるとこんな感じで、valid?をせず.errosを実行すると@messagesの中にエラー文が入ってきません。

2.6.3 :001 > contact = Contact.new()                                                                                                             
 => #<Contact id: nil, title: nil, body: nil, name: nil, email: nil, created_at: nil, updated_at: nil> 
2.6.3 :002 > contact.errors
 => #<ActiveModel::Errors:0x00007f44c8018ad8 @base=#<Contact id: nil, title: nil, body: nil, name: nil, email: nil, created_at: nil, updated_at: nil>, @messages={}, @details={}> 
2.6.3 :003 > contact.valid?
 => false 
2.6.3 :004 > contact.errors
 => #<ActiveModel::Errors:0x00007f44c8018ad8 @base=#<Contact id: nil, title: nil, body: nil, name: nil, email: nil, created_at: nil, updated_at: nil>, @messages={:title=>["タイトルを入力してください"], :body=>["内容を入力してください"], :name=>["名前を入力してください"], :email=>["メールアドレスを入力してください"]}, @details={:title=>[{:error=>:blank}], :body=>[{:error=>:blank}], :name=>[{:error=>:blank}], :email=>[{:error=>:blank}]}> 

ビュー

contact.html.erb
<% if contact.errors.messages.key?(:title) %>
 <%= contact.errors.messages[:title].first %>
<% end %>

<% if contact.errors.messages.key?(:body) %>
 <%= contact.errors.messages[:body].first %>
<% end %>

<% if contact.errors.messages.key?(:name) %>
  <%= contact.errors.messages[:name].first %>
<% end %>

<% if contact.errors.messages.key?(:email) %>
  <%= contact.errors.messages[:email].first %>
<% end %>

contact.errors.messages.key?(:title) : エラーがあったら実行する(条件分岐)
contact.errors.messages[:title].first : エラーメッセージの表示内容

下にあるrails consoleを参考にしてください。
contact.errors.messagesで全てのエラーメッセージを取得できます。なので.key?メソッド(ハッシュのkeyがあるか確認する。)を使い指定するカラムにエラーがあるか確かめ、エラーがあれば表示させるといった条件分岐になります。.firstの部分を記述しないと配列の中に入った状態で表示されてしまうので記述しています。

2.6.3 :003 > contact.valid?
 => false 
2.6.3 :004 > contact.errors
 => #<ActiveModel::Errors:0x00007f44c8018ad8 @base=#<Contact id: nil, title: nil, body: nil, name: nil, email: nil, created_at: nil, updated_at: nil>, @messages={:title=>["タイトルを入力してください"], :body=>["内容を入力してください"], :name=>["名前を入力してください"], :email=>["メールアドレスを入力してください"]}, @details={:title=>[{:error=>:blank}], :body=>[{:error=>:blank}], :name=>[{:error=>:blank}], :email=>[{:error=>:blank}]}> 
2.6.3 :004 > contact.errors.messages
 => {:title=>["タイトルを入力してください"], :body=>["内容を入力してください"], :name=>["名前を入力してください"], :email=>["メールアドレスを入力してください"]} 
2.6.3 :006 > contact.errors.messages.key?(:title)
 => true 
2.6.3 :007 > contact.errors.messages[:title]
 => ["タイトルを入力してください"] 
2.6.3 :008 > contact.errors.messages[:title].first
 => "タイトルを入力してください" 

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Routing Error解決のヒント

始めに

アプリ開発にはエラーはつきものです。
まずエラーが出た時の対処として、エラー文を読み解くことも大切ですが、
より早くエラーを解決するためには以下を考えることが必要です。

エラーが出てしまった☹︎

①何をしたらエラーになったのか考える

②エラー文を解読する(どこの記述がエラーなのか読み解く)

③どこがどう間違えている可能性があるのか仮説を立てる

Routing Error = ルーティングがおかしい

文のそのままの意味でルーティングが通常通りに機能していないエラー文です。タイトルの下には No route matches [HTTP] "URIパターン" の記載があり、エラーになっているHTTPとURIを教えてくれています。まずはそこを確認します。また、その以下の表には rails routesコマンドを行なった時と同じ一覧があるのでエラーになっているHTTP、URIパターンと比べることができます。
Image from Gyazo

解決のヒント

①何をした時にエラーが起こっているのかエラー文にあるHTTPとURIを使って調べる
- 異なるHTTPがあれば、そこのルーティングが動いた時にエラーがあることがわかる
②ルーティングの記述が間違っていないか(routes.rbファイル)
- do~endは囲まれているのか
- resourceの書き方、スペルに間違いはないか
③viewのパスに間違いはないのか(form_with,like_to,aのリンク先など)
- リンクの飛ぶ先のパスはあっているのか
- 情報の受け渡しは行われているのか
- スペル間違いはないか(@やsのつけ忘れ)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MySQLを使ったRailsアプリをHerokuにデプロイしようとしてエラーになった話

開発環境

  • Ruby(3.0.0)
  • Ruby on Rails(6.1.3)
  • MySQL(8.2.3)

エラーになった背景

heroku run rails db:migrate

上記のコマンドを入力した時に、下記のエラーが発生。

Mysql2::Error::ConnectionError: Can’t connect to local MySQL server through socket ‘/tmp/mysql.sock’ (2)

エラーの原因

HerokuのデフォルトのデータベースがPostgresqlであるため、MySQLを使う場合は下準備が必要であるため。

解決方法

ClearDBというアドオンを使う

heroku addons:create cleardb:ignite

上記のコマンドを入力します。
すると、ClearDBというアドオンを使うことができるようになります。

Mysqlへの接続

次に、Rails側でそのMySQLデータベースの情報を知る必要がありますが、Railsは

DATABASE_URL

という環境変数が設定されていると、その情報を使ってデータベースに接続するという仕様になっているため、

heroku config:add DATABASE_URL='mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true'

でMySQLの情報を設定してあげると、その情報を使ってMySQLに接続できる、ということになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像がアップデートされない。。

refile(Gem)を使って画像投稿をしていて、
マイページでプロフィール画像の編集できるようにしたが、
なぜか画像が更新されない。

editアクション、updateアクションもやっている。

def edit
    @user = User.find(params[:id])
end

def update
  @user = User.find(params[:id])
  @user.update(user_params)
  redirect_to user_path(@user.id)
end

viewもこんな感じ

<%= attachment_image_tag @user, :profile_image, :fill, 60, 60, class: "img-circle pull-left profile-thumb", fallback: "no_image.jpg"%>

環境

・docker
・Rails

試したこと

・ブラウザにキャッシュされているのかな?と思いキャッシュを削除。
・fillの部分をsizeに変更してみましたが出来ない。

解決

docker-compose runをしてみたら、なぜか出来た。

⇨新しいコンテナが作られたからかな?
⇨最初、docker-copose up -d で放置していて、それから新しいコンテナを作っていなかったから?
⇨まだ頭のモヤモヤが。。。

とりあえず、解決したので、進みます。もやもやがありますが、、、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactとaxiosを用いて非同期でサインアウトする

実装方針

  • devise-auth-tokenで使用する、uid, client, access-tokenの3つが全て存在する場合のみサインアウトする
  • csrf-tokenのチェックを事前に行う
  • client, tokenをdestroyアクション内で削除
  • csrf-token,sessionをafter_actionで削除

フロントエンド

リクエストの送信

getCsrfToken() {
    if (!(axios.defaults.headers.common['X-CSRF-Token'])) {
      return (
        document.getElementsByName('csrf-token')[0].getAttribute('content')
      )
    } else {
      return (
        axios.defaults.headers.common['X-CSRF-Token']
      )
    }
  };


setAxiosDefaults() {
    axios.defaults.headers.common['X-CSRF-Token'] = this.getCsrfToken();
  };

userAuthentification() {
    if (axios.defaults.headers.common['uid'] && axios.defaults.headers.common['client'] && axios.defaults.headers.common['access-token']) {
      axios.defaults.headers.common['uid']
      axios.defaults.headers.common['client']
      axios.defaults.headers.common['access-token']
    } else {
      return null
    }
  }


if (this.props.content == 'SignOut') {
      this.setAxiosDefaults();
      this.userAuthentification()
      axios
      .delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})
  this.setAxiosDefaults();
  this.userAuthentification()

この2行でそれぞれcsrf-tokenとuser情報をrequest.headersにセットする。後者は新規登録時に発行される情報で、devise-auth-tokenを用いると発行されるもの。

def update_auth_header 
    @token = @user.create_token
    return unless @user && @token.client
    @token.client = nil unless @used_auth_by_token
    if @used_auth_by_token && !DeviseTokenAuth.change_headers_on_each_request
      auth_header = @user.build_auth_header(@token.token, @token.client)
      response.headers.merge!(auth_header)  
    else
      unless @user.reload.valid?
        @user = @user.class.find(@user.to_param) 
        unless @user.valid?
          raise DeviseTokenAuth::Errors::InvalidModel, "Cannot set auth token in invalid model. Errors: #{@resource.errors.full_messages}"
        end
      end
      refresh_headers
    end
  end

これを新規登録後に読み込むことで request.headerにclient、tokenをmerge。uidはデフォルトでresponse.headersに含まれている。

      .delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})

ここは普段どおりのaxiosのリクエスト。パラメータにはuserの識別子となるuidを渡す。

サーバーサイド

def destroy  
  @user = User.find_for_database_authentication(uid: request.headers['uid'])
  @token = request.headers['access-token']
  @client = request.headers['client']
  if @user && @client && @token
    @token.clear 
    @client = nil
    render_destroy_success
  else
    render_destroy_error
  end
end

リクエストヘッダーからuid(email)を取り出してインスタンスをDBから参照し、token、clientもそれぞれリクエストヘッダーから定義する。
user,token,client全てが存在しないとログアウトできないようにしている。
次にtokenとclientを削除することでクライアント側でuserが存在しない状態にし、ログアウト処理完了。

destroyアクション後にafter_actionで以下を読み込む

after_action :set_csrf_token_header
after_action :reset_session, only: [:destroy]

これでcsrf-tokenとsessionを削除

挙動

recommended-books-2-2.gif

感想

結局deviseのコントローラー0から改造することになってしまった。
まだ処理内容わかっていない点も多いのでこの後コメントアウトをつける作業に移る。

参考

devise-auth-token公式
https://github.com/lynndylanhurley/devise_token_auth/blob/master/app/controllers/devise_token_auth/sessions_controller.rb

次やること

  • 結合テスト
  • コメントアウトつける
  • react-routerの導入
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Reactとaxiosを用いて非同期でdevise-auth-tokenを利用したのサインアウトを行う

実装方針

  • devise-auth-tokenで使用する、uid, client, access-tokenの3つが全て存在する場合のみサインアウトする
  • csrf-tokenのチェックを事前に行う
  • client, tokenをdestroyアクション内で削除
  • csrf-token,sessionをafter_actionで削除

フロントエンド

リクエストの送信

getCsrfToken() {
    if (!(axios.defaults.headers.common['X-CSRF-Token'])) {
      return (
        document.getElementsByName('csrf-token')[0].getAttribute('content')
      )
    } else {
      return (
        axios.defaults.headers.common['X-CSRF-Token']
      )
    }
  };


setAxiosDefaults() {
    axios.defaults.headers.common['X-CSRF-Token'] = this.getCsrfToken();
  };

userAuthentification() {
    if (axios.defaults.headers.common['uid'] && axios.defaults.headers.common['client'] && axios.defaults.headers.common['access-token']) {
      axios.defaults.headers.common['uid']
      axios.defaults.headers.common['client']
      axios.defaults.headers.common['access-token']
    } else {
      return null
    }
  }


if (this.props.content == 'SignOut') {
      this.setAxiosDefaults();
      this.userAuthentification()
      axios
      .delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})
  this.setAxiosDefaults();
  this.userAuthentification()

この2行でそれぞれcsrf-tokenとuser情報をrequest.headersにセットする。後者は新規登録時に発行される情報で、devise-auth-tokenを用いると発行されるもの。

def update_auth_header 
    @token = @user.create_token
    return unless @user && @token.client
    @token.client = nil unless @used_auth_by_token
    if @used_auth_by_token && !DeviseTokenAuth.change_headers_on_each_request
      auth_header = @user.build_auth_header(@token.token, @token.client)
      response.headers.merge!(auth_header)  
    else
      unless @user.reload.valid?
        @user = @user.class.find(@user.to_param) 
        unless @user.valid?
          raise DeviseTokenAuth::Errors::InvalidModel, "Cannot set auth token in invalid model. Errors: #{@resource.errors.full_messages}"
        end
      end
      refresh_headers
    end
  end

これを新規登録後に読み込むことで request.headerにclient、tokenをmerge。uidはデフォルトでresponse.headersに含まれている。

      .delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})

ここは普段どおりのaxiosのリクエスト。パラメータにはuserの識別子となるuidを渡す。

サーバーサイド

def destroy  
  @user = User.find_for_database_authentication(uid: request.headers['uid'])
  @token = request.headers['access-token']
  @client = request.headers['client']
  if @user && @client && @token
    @token.clear 
    @client = nil
    render_destroy_success
  else
    render_destroy_error
  end
end

リクエストヘッダーからuid(email)を取り出してインスタンスをDBから参照し、token、clientもそれぞれリクエストヘッダーから定義する。
user,token,client全てが存在しないとログアウトできないようにしている。
次にtokenとclientを削除することでクライアント側でuserが存在しない状態にし、ログアウト処理完了。

destroyアクション後にafter_actionで以下を読み込む

after_action :set_csrf_token_header
after_action :reset_session, only: [:destroy]

これでcsrf-tokenとsessionを削除

挙動

recommended-books-2-2.gif

感想

結局deviseのコントローラー0から改造することになってしまった。
まだ処理内容わかっていない点も多いのでこの後コメントアウトをつける作業に移る。

参考

devise-auth-token公式
https://github.com/lynndylanhurley/devise_token_auth/blob/master/app/controllers/devise_token_auth/sessions_controller.rb

次やること

  • 結合テスト
  • コメントアウトつける
  • react-routerの導入
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む