20200318のRailsに関する記事は19件です。

ローカルで行った変更点が本番環境に反映されない時の対処法

せっかくローカルで変更して上手くいっても、本番環境で変化がなければなんの意味もありません。

今回、ローカルホストではしっかり変わっているにも関わらず、本番環境(capistranoで自動デプロイ時)に反映されない時の対処法を書いていきます。

結論「unicornをkill」しましょう。

unicornをkillする手順

1.ターミナル

[ec2-user@本番環境 <リポジトリ名>]$ ps aux | grep unicorn

こちらをしていただくと

ec2-user 17877  0.4 18.1 588472 182840 ?       Sl   01:55   0:02 unicorn_rails master -c config/unicorn.rb -E production -D
ec2-user 17881  0.0 17.3 589088 175164 ?       Sl   01:55   0:00 unicorn_rails worker[0] -c config/unicorn.rb -E production -D
ec2-user 17911  0.0  0.2 110532  2180 pts/0    S+   02:05   0:00 grep --color=auto unicorn

このように表記されると思います。

一番上の行の5桁の数字(PID)をkillします。

kill -9 上記で確認したPID

あとはもう一度デプロイ するだけ

でも、毎回デプロイ のたびにこれをするのは手間ですよね。

なので、デプロイ 時に自動でunicornをkillしてstartしてくれる記述を書きましょう。

config/deploy.rb

after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:stop'
    invoke 'unicorn:start'
  end

  desc 'upload master.key'
  task :upload do
    on roles(:app) do |host|
      if test "[ ! -d #{shared_path}/config ]"
        execute "mkdir -p #{shared_path}/config"
      end
      upload!('config/master.key', "#{shared_path}/config/master.key")
    end
  end
  before :starting, 'deploy:upload'
  after :finishing, 'deploy:cleanup'
end

こちらの

namespace :deploy do
  task :restart do
    invoke 'unicorn:stop'
    invoke 'unicorn:start'
  end

このように記述することで、デプロイ 時に自動でunicornを再起動してくれます。

私のような誰かの助けになれば幸いです!!!

それではまたどこかで^^

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

Sequel Proに接続できなかったときの備忘録

はじめに

dockerで起動したmysqlコンテナSequel Proの接続が出来なかったので備忘録として記録します。

設定

docker-compose.yml
#一部抜粋
db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: root
    ports:
      - "3306:3306"
SequelPro
名前:local(任意)
ホスト:127.0.0.1
ユーザ名:root
パスワード:password
ポート:3306

接続できずにエラー発生。

image.png

ググる。
ポート番号がすでにローカルで使われているかもしれない!という記事にたどり着く。
dockerで起動したmysqlコンテナにsequel proで接続する

ポートの確認

ポート番号の確認をしてみる。(portscanを実行する)
ローカルで使用されているポート番号の調べ方はこちらの記事を参考にしました。
Macで使用しているポートを調べる方法とポートを使用しているプログラム確認方法

以前にローカルで使用した
mysqlですでに3306番が使われていることが発覚!

ポート番号の変更

docker-compose.yml
#一部抜粋
db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: root
    ports:
      - "4306:3306"     ←←変更

変更後、再度docker-compose buildをやりなおしコンテナを立ち上げ、Sequel Proへの接続が無事に成功しました。

締め

いろいろ調べていくとmysql8.0にするとSequelProへの接続がエラーになったという情報もちらほら見られた。mysql8.0は現時点では情報量も少ないので、mysql5.7の方が扱いやすいかもですね。

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

railsで動画サイトにいいね機能実装

動画サイトにいいね機能を実装してみた

今では当たり前になりつつあるいいね機能ですが、どのように作られているか気になりポートフォリオに試しに実装してみました。1ユーザーが複数の動画に対していいねをし、また1動画に対して複数のユーザーがいいねする、いわば多対多の関係なので、ユーザーと動画の中間テーブルとしていいねテーブルを作成していきます。

完成図

image.png

環境

rails : v5.2.4.1

〜実装済み機能〜
・動画(一覧表示、新規投稿、詳細、編集、削除)
・ユーザー(新規登録、ログイン、編集、ログアウト、詳細)
・ページネーション

実装手順

※いいね機能に関する部分のみコメントアウトで説明書きしています。

  • ターミナルからLikeモデル作成
$ rails g model Like user:references video:references
$ rails db:migrate
models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  validates :name, presence: true, uniqueness: true
  has_many :videos, dependent: :destroy
  # いいねはユーザーのdestroyに依存
  has_many :likes, dependent: :destroy
  # ユーザーがいいねしている動画
  has_many :liked_videos, through: :likes, source: :video
  # いいねしているかどうかを判定
  def already_liked?(video)
    self.likes.exists?(video_id: video.id)
  end
end
models/video.rb
class Video < ApplicationRecord
  validates :name, :work, presence: true
  belongs_to :user
  # いいねは動画のdestroyに依存
  has_many :likes, dependent: :destroy
  # 動画にいいねしているユーザー
  has_many :liked_users, through: :likes, source: :user

  mount_uploader :work, VideoUploader
end
models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :video
  # 1人が1つの動画に1いいね
  validates_uniqueness_of :video_id, scope: :user_id
end
  • ターミナルからlikesコントローラー作成
$ rails g controller likes
  • 「いいねする」「いいねを取り消す」だけなのでcreate、destroyのみ追加
config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root "videos#index"
  resources :users, only: [:edit, :update, :show]
  # いいねを動画にネストさせる
  resources :videos do
    resources :likes, only: [:create, :destroy]
  end
end
controllers/likes_controller.rb
class LikesController < ApplicationController

  def create
    # 今ログインしているユーザーによるいいね
    @like = current_user.likes.create(video_id: params[:video_id])
    # 今いる画面にリダイレクト
    redirect_back(fallback_location: root_path)
  end

  def destroy
    # 今ログインしているユーザーがいいねしている動画を探す
    @like = Like.find_by(video_id: params[:video_id], user_id: current_user.id)
    # いいねを取り消す
    @like.destroy
    # 今いる画面にリダイレクト
    redirect_back(fallback_location: root_path)
  end

end
controllers/videos_controller.rb
class VideosController < ApplicationController

  def index
    @videos = Video.includes(:user).page(params[:page]).order("created_at DESC").per(12)
  end

  def new
    @video = Video.new
  end

  def create
    Video.create(video_params)
    redirect_to root_path
  end

  def show
    @video = Video.find(params[:id])
    # いいねする
    @like = Like.new
  end

  def edit
    @video = Video.find(params[:id])
  end

  def update
    video = Video.find(params[:id])
    video.update(video_params)
  end

  def destroy
    video = Video.find(params[:id])
    video.destroy
    redirect_to root_path
  end

  private

  def video_params
    params.require(:video).permit(:name, :work).merge(user_id: current_user.id)
  end

end

end

ログインしていないユーザーもいいね数を見れるようにする場合はこのように分岐が必要

show.html.haml
-# ログインしている場合
- if user_signed_in?
  -# かつ既にいいねしている場合
  - if current_user.already_liked?(@video)
    = link_to video_like_path(@video), method: :delete do
      = icon('fa', 'heart', class: 'content__show__box__top__icons__heart__already')
  -# かつまだいいねしていない場合
  - else
    = link_to video_likes_path(@video), method: :post do
      = icon('far', 'heart', class: 'content__show__box__top__icons__heart__yet')
-# ログインしていない場合
- else
  = icon('far', 'heart', class: 'content__show__box__top__icons__heart__yet')
    = @video.likes.count

videoのindexコントローラーではvideoのidカラムが取得できず、エラーでnilが返されてしまう。
そのため一覧画面では表示のみとし、いいねボタンは動作しないように設定。

index.html.haml
-# ログインしている場合、かつ既にいいねしている場合
- if user_signed_in? && current_user.already_liked?(video)
  = icon('fa', 'heart', class: 'content__box__top__icons__heart__already')
-# それ意外の場合
- else
  = icon('far', 'heart', class: 'content__box__top__icons__heart__yet')
    = video.likes.count

課題

  • 動画の一覧表示(index)、ユーザーごとの詳細ページ(users/show)でもいいねできるようにする
  • いちいち画面更新せず非同期で反映されるようにJqueryもしくはVueに書き換え

参考

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

【rails db:createエラー】dependent dylib '/usr/local/opt/mysql/lib/libssl.1.1.dylib' not found for '/Library/Ruby/Gems/2.6.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.bundle'

エラーをいろいろ試して解決しました。(めちゃくちゃな解決法ですが初心者なのでご容赦ください)

エラー内容

$ rails db:create

したところ

LoadError: dlopen(/Library/Ruby/Gems/2.6.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.bundle, 0x0009): dependent dylib '/usr/local/opt/mysql/lib/libssl.1.1.dylib' not found for '/Library/Ruby/Gems/2.6.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.bundle' - /Library/Ruby/Gems/2.6.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.bundle

のエラーが発生。
'/usr/local/opt/mysql/lib/libssl.1.1.dylib'のファイルが無い?と書いてありました。

解決方法

他の方の参考にはあまりならないかもしれません。。。

homebrewでmysqlをインストールすれば、/usr/local/opt/mysql/lib/libssl.1.1.dylibファイルができると思い、
https://qiita.com/narikei/items/cd029911597cdc71c516
を参考に

$ brew install mysql
$ mysql.server start
$ bundle init
$ bundle install --path=vendor/bundle

を実施しました。

その後、アプリ内で改めて

$ rails db:create

したら、

Could not find mysql2-0.5.3 in any of the sources
Run `bundle install` to install missing gems.

というエラーが出たので、

$ bundle install

したら、

An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/&#039;` succeeds before bundling.

のエラーが出ました。
https://qiita.com/fukuda_fu/items/463a39406ce713396403
を参考に

$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl@1.1/include"
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"
$ bundle install

したところ、成功しました。改めて、

$ rails db:create

したところ、

Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

のエラーが出たので、
https://qiita.com/carotene4035/items/e00076fe3990b9178cc0
を参考に

$ touch /tmp/mysql.sock
$ mysql.server restart

をした後

$ rails db:create

で無事にデータベースが作成できました。

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

【Rails】has_secure_password

has_secure_password

user.rb
class User < ApplicationRecord
  .
  .
  .
  has_secure_password
end

上記のようにユーザモデルに定義すると機能します。

機能

Railsのメソッド、下記のような機能を持っています。

・セキュアなハッシュ化されたパスワードをpassword_digestカラムに保存する。
・passwordとpassword_confirmationという2つの仮想的な属性を追加する。
validations: trueを記載することで存在性と値が一致するかどうかのバリデーションが追加される。
(「password」は存在しているか、「password」と「password_confirmation」は同じか)
・authenticateメソッドが使えるようになる。
(引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalseを返すメソッド)

前提条件

モデル内にpassword_digestという属性が含まれていないとhas_secure_passwordの機能は動作しない。

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

Psych::SyntaxErrorの対処法

エラーの内容

s3に画像をアップさせる関係で、secrets.ymlを書き換えたら、rails sで以下のエラーが出た。

`parse': (<unknown>): did not find expected key while parsing a block mapping at line 13 column 1 (Psych::SyntaxError)

解決した方法

上記のエラーについて調べたところ、検索で上位表示される記事のほとんどで、ymlファイルのインデントがずれていることによるエラーが出ると書いてあった。

そこで、筆者はVScodeを利用していることから、(command)+Pを用いて、.ymlのつくファイルを検索しインデントのずれを探した。

まあ、.ymlファイルをいじったことは記憶に新しかった(s3関連でいじってる)ので、secrets.ymlをまず確認したところ、development、test、productionが全てずれていた。

これを戻し、rails sをし直すと、、、

できた!!!!!

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

【Rails】URLテキストにaタグを自動でつける

はじめに

ユーザーがフォームに投稿したURLテキストに自動でaタグをつけて表示できたらなーーと調べていたら、uriライブラリーのおかげでとても簡単にできたので、備忘録としてまとめます。

uriライブラリーを読み込む

Rubyの標準ライブラリーとして'uri'というURIを扱う標準ライブラリ-があるので、読み込みます。
https://docs.ruby-lang.org/ja/latest/class/URI.html

app/helpers/application_helper.rb
require "uri"

ヘルパーメソッドを作成

uriライブラリーを読み込んだapplication_helper.rbでメソッドを作成します。

app/helpers/application_helper.rb
require "uri"
#追加
def text_url_to_link(text)

  URI.extract(text, ['http', 'https']).uniq.each do |url|
    sub_text = ""
    sub_text << "<a href=" << url << " target=\"_blank\">" << url << "</a>"

    text.gsub!(url, sub_text)
  end

  return text
end
  1. URI.extractで、httpもしくはhttpsで始まるtextを url として取り出し、sub_text という変数に代入

2.
gsub!メソッドで textをsub_textに変換

3.
変換したtextを返す

該当のviewに表示させる

URLテキストにaタグをつけて表示させたい部分に、以下のように先ほど作成したメソッドを使用して、表示させます。

app/views/sample.html.erb
<%= text_url_to_link(h(該当する変数)).html_safe %>

これで、無事にURLテキストに自動でaタグをつけて表示してくれるようになります。

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

railsで学ぶテスト処理(rails チュートリアルで学んだことをまとめてみました)

この記事の説明について

この記事ではrails チュートリアルで学んだことを私なりにまとめてみました。
理解の浅い部分がありますので、間違いがある場合はどうぞご教授ください。

テストとは

開発をしながら、動作するかテストすること

テストの種類

・モデルテスト→モデルが機能している
・機能テスト→コントローラーとビューの連携
・統合テスト→ユーザー目線でうまく動作するか、全体的にテストって感じ?

それぞれ別のテストファイルにテストを書く

assert 内容 →内容通りならば成功、でなければ失敗

assert not 内容 →内容通りでなければ成功、内容通りならば失敗

assertに関してはこちらのサイトがわかりやすかったです。
Rails チュートリアル 【初心者向け】 テストを10分でおさらいしよう!

主な流れ

まず開発する前にテストの内容をtestfileに記述する
(ここで書くテスト内容はまだやっていない開発内容に関する事→開発しないとテストは成功しない)

開発をする(テストが成功する予定)

自動でテストor手動でコンソール内でrails test

補足

全てのtestはtest_helperを導入している
test_helperはtestディレクトリーの中にある。
test_helperの中にはtestで使う関数を書いている。
またapplication_helperをtest _helperの中に導入することで、実際の開発環境で使っている関数(例 login関数)をtestで使用できるようになる。

自動テスト

その中でも開発と同時並行で自動でテストする方法がある。開発で変更を加えるたびに自動でテストを子なってくれる。

自動テストを導入するためには
テストを自動でやってくれるGuardを取り入れる

Guard fileに自動テストするように指示を書く
詳しくはRuby on Rails チュートリアル

注意
Guardを使うときにspringという機能を使う
しかしspringがgitとの競合をしないように、.git ignoreファイルに、springを書きgitリポジトリに保存されないようにする

モデルテスト

モデル名test.rbに記述するテスト。(例 test/models/usertest.rb)

rails g model ~でモデルが作られると
testディレクトリにそのモデルのためのテストができる

書き方
まずモデルのインスタンスを作成する(主にseup関数で行う)

そのインスタンスが機能しているかや、複数のインスタンスの関係性が機能しているかなど確認する。

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup #インスタンス作成
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "name should be present" do
    @user.name = "     "
    assert_not @user.valid? #このテストの場合Userファイルで書いたvalidateが発動しているかテストしている
  end
end

機能テスト

controller_test.rbに記述するテスト。

rails g controller ~でコントローラーが作られると
testディレクトリにそのコントローラーのためのテストができる

書き方
まず必要な処理をsetupで書く

どのアクションを発動させるためにどのURLを発信するかを書き、assert関数でテスト内容を書く

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
  
  
  end

  test "should get new" do
    get users_new_url
    assert_response :success
  end
end

統合テスト

test/integration/統合テスト名_test.rbに記述するテスト。

rails g integration_test 〜でtestディレクトリに作られる。

書き方
まず必要な処理をsetupで書く

どのアクションを発動させるためにどのURLへ発信するかを書き、assert関数でテスト内容を書く
様々なURLへ発信して、如何にも実際に手で動かしているテストのように行う。

test/integration/microposts_interface_test.rb
require 'test_helper'

class MicropostsInterfaceTest < ActionDispatch::IntegrationTest

  def setup
  
  
  end

  test "micropost interface" do
  get root_path
  assert ...
  
  
  post microposts_path ...
  assert ...
  
  
  end
機能テストとの違いは一つのコントローラー内でのテストではなく、いろんなコントローラーに繋いでどうなるかなどをテストできる?

以下のサイトでいう結合テスト(内部)が機能テストで結合テスト(外部が)統合テスト?
単体テスト・結合テスト・総合テストの違い、観点や注意点を簡単に説明する

デバッグとは

デバッグとはバグをとる事

どんなバグが起きているか実行結果から知りたい場合

開発環境のみで、全てのレイアウトでparamsの中身が見れるようにする。
debug(params)とする→これでparamsの中身を見るという指示になる
applicationHTMLで共通レイアウトを作るときに、if文で開発環境にのみバクの内容が見れるようにする
詳しくはRuby on Rails チュートリアル

補足

fixturesとは

テストで使うためのデータベースにあるデータとして記述される。

ログインを実践する統合テストをしたい時
test.fixturesファイルに、ログインユーザーの情報を置く。ここにはテストで使用するユーザーの情報を置く

統合テスト
詳しくはRuby on Rails チュートリアルでfixtureで検索

テストで作ったインスタンスの値を知りたい時

テスト内で
assigns(インスタンス変数).データカラムとするとアクセスできる。
詳しくはRuby on Rails チュートリアルでassignsで検索

参考文献

参考文献:
Rails チュートリアル
Railsガイド
Ruby on Rails5 アプリケーションプログラミング
単体テスト・結合テスト・総合テストの違い、観点や注意点を簡単に説明する
Rails チュートリアル 【初心者向け】 テストを10分でおさらいしよう!

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

railsで学ぶテスト処理(rails チュートリアル)

この記事の説明について

この記事ではrails チュートリアルで学んだことを私なりにまとめてみました。
理解の浅い部分がありますので、間違いがある場合はどうぞご教授ください。

テストとは

開発をしながら、動作するかテストすること

テストの種類

・モデルテスト→モデルが機能している
・機能テスト→コントローラーとビューの連携
・統合テスト→ユーザー目線でうまく動作するか、全体的にテストって感じ?

それぞれ別のテストファイルにテストを書く

assert 内容 →内容通りならば成功、でなければ失敗

assert not 内容 →内容通りでなければ成功、内容通りならば失敗

assertに関してはこちらのサイトがわかりやすかったです。
Rails チュートリアル 【初心者向け】 テストを10分でおさらいしよう!

主な流れ

まず開発する前にテストの内容をtestfileに記述する
(ここで書くテスト内容はまだやっていない開発内容に関する事→開発しないとテストは成功しない)

開発をする(テストが成功する予定)

自動でテストor手動でコンソール内でrails test
で成功する

必要ならばリファクタリング

リファクタリングしてもエラー出ないかテスト

補足

全てのtestはtest_helperを導入している
test_helperはtestディレクトリーの中にある。
test_helperの中にはtestで使う関数を書いている。
またapplication_helperをtest _helperの中に導入することで、実際の開発環境で使っている関数(例 login関数)をtestで使用できるようになる。
詳しくはRuby on Rails チュートリアル

自動テスト

その中でも開発と同時並行で自動でテストする方法がある。開発で変更を加えるたびに自動でテストを行ってくれる。

自動テストを導入するためには
テストを自動でやってくれるGuardを取り入れる

Guard fileに自動テストするように指示を書く
詳しくはRuby on Rails チュートリアル

注意
Guardを使うときにspringという機能を使う
しかしspringがgitとの競合をしないように、.git ignoreファイルに、springを書きgitリポジトリに保存されないようにする

モデルテスト

モデル名test.rbに記述するテスト。(例 test/models/usertest.rb)

rails g model ~でモデルが作られると
testディレクトリにそのモデルのためのテストができる

書き方
まずモデルのインスタンスを作成する(主にseup関数で行う)

そのインスタンスが機能しているかや、複数のインスタンスの関係性が機能しているかなど確認する。

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup #インスタンス作成
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "name should be present" do
    @user.name = "     "
    assert_not @user.valid? #このテストの場合Userファイルで書いたvalidateが発動しているかテストしている
  end
end

機能テスト

controller_test.rbに記述するテスト。

rails g controller ~でコントローラーが作られると
testディレクトリにそのコントローラーのためのテストができる

書き方
まず必要な処理をsetupで書く

どのアクションを発動させるためにどのURLを発信するかを書き、assert関数でテスト内容を書く

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
  
  
  end

  test "should get new" do
    get users_new_url
    assert_response :success
  end
end

統合テスト

test/integration/統合テスト名_test.rbに記述するテスト。

rails g integration_test 〜でtestディレクトリに作られる。

書き方
まず必要な処理をsetupで書く

どのアクションを発動させるためにどのURLへ発信するかを書き、assert関数でテスト内容を書く
様々なURLへ発信して、如何にも実際に手で動かしているテストのように行う。

test/integration/microposts_interface_test.rb
require 'test_helper'

class MicropostsInterfaceTest < ActionDispatch::IntegrationTest

  def setup
  
  
  end

  test "micropost interface" do
  get root_path
  assert ...
  
  
  post microposts_path ...
  assert ...
  
  
  end
機能テストとの違い

違いは一つのコントローラーのアクション内でのテストではなく、rootingを通して様々なコントローラーに繋いでどうなるかなどをテストできる?
以下のサイトでいう結合テスト(内部)が機能テストで結合テスト(外部が)統合テスト?
単体テスト・結合テスト・総合テストの違い、観点や注意点を簡単に説明する

単体テスト

上記のテストでは実施しにくいテスト
helpersの中で書くことが多い?
例: 関数で読み出された文字列に誤字がないかとか

デバッグとは

デバッグとはバグをとる事

どんなバグが起きているか実行結果から知りたい場合

開発環境のみで、全てのレイアウトでparamsの中身が見れるようにする。
debug(params)とする→これでparamsの中身を見るという指示になる
applicationHTMLで共通レイアウトを作るときに、if文で開発環境にのみバクの内容が見れるようにする
詳しくはRuby on Rails チュートリアル

補足

fixturesとは

テストで使うためのデータベースにあるデータとして記述される。

ログインを実践する統合テストをしたい時
test.fixturesファイルに、ログインユーザーの情報を置く。ここにはテストで使用するユーザーの情報を置く

統合テスト
詳しくはRuby on Rails チュートリアルでfixtureで検索

テストで作ったインスタンスの値を知りたい時

テスト内で
assigns(インスタンス変数).データカラムとするとアクセスできる。
詳しくはRuby on Rails チュートリアルでassignsで検索

テストファイルのsetup関数で変数を作り、テストに採用したいときには変数は@が頭についておかないといけない。

test/controllers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  def setup
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Home | #{@base_title}"
  end

参考文献

参考文献:
Rails チュートリアル
Railsガイド
Ruby on Rails5 アプリケーションプログラミング
単体テスト・結合テスト・総合テストの違い、観点や注意点を簡単に説明する
Rails チュートリアル 【初心者向け】 テストを10分でおさらいしよう!

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

Docker環境にSystem Specを導入する

はじめに

Docer環境でRSpecのSystem Specを導入しようとしたところ、結構ハマったので、備忘録としてまとめます。

まず、Docer環境にSystem Specを実行するためには、いくつか方法があるらしい。
調査をしていると、メジャーな方法は以下の二つ(もっとあるかもしれませんが、、、)

1. Railsが動いているimageにchromeをインストールする方法
2. chrome用コンテナを立ち上げる方法

今回は2の方法でやってみました。

前提

Quickstart: Compose and Railsの通りに、Rails on Dockerの環境構築が済んでいる状態とします。
筆者の環境は
- Ruby 2.5.7
- Rails 5.2.4
です。

docker-compose.ymlを編集する

selenium_chromeのコンテナが立ち上がるようdocker-compose.ymlに追加していきます。
Dockerイメージにはselenium/standalone-chromeを使用します。

docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db
      #追加
      - chrome
  #追加
  chrome:
    image: selenium/standalone-chrome:3.141.59-dubnium
    ports:
      - 4444:4444

gemを追加する

Gemfile
group :development, :test do
  gem 'rspec-rails'
end

group :test do
  gem 'capybara', '>= 2.15'
  gem 'selenium-webdriver'
end

dockerをbuildして、bundle installします。

$ docker-compose build
$ docker-compose up -d

rspecの設定

rspecをインストールします。

$ docker-compose run web rails g rspec:install

headless chromeの設定

spec/rails_helper.rbにheadless chromeの設定を追加していきます。

/spec/rails_helper.rb
require 'capybara/rspec'

# headless chrome 設定①
Capybara.server_host = Socket.ip_address_list.detect { |addr| addr.ipv4_private? }.ip_address
Capybara.server_port = 3001   

Capybara.register_driver :selenium_remote do |app|
  url = "http://chrome:4444/wd/hub"
  opts = { desired_capabilities: :chrome, browser: :remote, url: url }
  Capybara::Selenium::Driver.new(app, opts)
end


 # headless chrome 設定②
RSpec.configure do |config|

  config.before(:each, type: :system) do
    driven_by :rack_test
  end

  config.before(:each, type: :system, js: true) do
    driven_by :selenium_remote
    host! "http://#{Capybara.server_host}:#{Capybara.server_port}"
  end
end

ポイント:
後半部分(# headless chrome 設定②)で、
js: trueを記述した場合のみ、seleniumドライバーが立ち上がるように設定しています。

ハマったポイント:
js: trueのテストを走らせたとき、Capybaraがseleniumサーバーを立ち上げて、先ほど設定したchromeコンテナで起動しているchromeを操作します。
しかし、Capybara.server_portを指定する部分(*)で、webコンテナで指定したポートと同じ3000を指定してしまうと、js: trueのテストを走らせたときに、webコンテナで指定したポートと競合してseleniumサーバーが立ち上がらずエラーとなってしまいます。
そこで、Capybara.server_port = 3000 3001
とすることで、競合することなく、無事サーバーが立ち上がり、テストが通るようになりました。

これで準備は整いました。
あとは、テストを実際に走らせるだけです。

実際にテストを走らせてみる

spec/system/test_spec.rb
require "rails_helper"

RSpec.describe 'Test', type: :system, js: true do
  example 'サンプルテスト' do
    #ここにテスト内容を記述
  end
end
$ docker-compose exec web bundle exec rpsec

これで、テストが通るはずです。

参考記事

下記の記事を参考にさせていただきました。
ありがとうございます。
- Docker で RSpec の System Spec を実行するための設定メモ
- Rails + Selenium + DockerでSystemSpecの環境構築
- Rails on Dockerにて、Headless ChromeでSystem Testをやってみた。

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

rails検索機能追加

はじめに

今回は投稿されたマイクロポストを入力された文字からあいまい検索する機能を追加します。

作るもの

railsのform_withヘルパーを利用した投稿の検索機能。(題材は自分のポートフォリオ)

対象読者

railsチュートリアル終了後等に何か機能を追加したい人等。

作成の流れ

1.対応するビューの作成
2.コントローラーの編集

1.対応するビューの作成

今回は検索フォームをroot_path上に設けます。(自分のポートフォリオがマイクロポストの一覧をroot_path(static_pages/home)に設けているため)

app/views/static_pages/home.html.erb
<%= form_with( url: root_path, class: 'search_form', method: :get, local:true) do |f| %>
  <%= f.text_field :search,class: 'field',value: params[:search]\
  placeholder: "スレ・コメント検索"%>
<%= f.submit '検索', class: "btn" %>

form_withを利用して検索フォームを作ります。各値は
url:root_path検索後に表示するページ
method:get(httpメゾット。今回はページの取得なのでget)
local:true(ajax処理(非同期通信)を無効にする。デフォルトではajaxで処理する。)
text_field: search 入力フォームになります。
value: params[:search]と値を設定しておくことで検索後も値を保持します。(URLのクエリから取得。)
あとはget動作を開始させるsubmitを配置します。

検索ボタンを押せばurlのページを表示しようとするので検索の処理は対応するコントローラーのアクション内に書きます。

2.コントローラーの編集

コントローラーで送られた値を処理しますが、コントローラー内に処理を全て書くとごちゃごちゃしてしまいますのでDBとやりとりをする箇所はmodel側で記述します。

app/controller.rb
    @microposts =  params[:search].present? ? Micropost.micropost_serach(params[:search]) :  Micropost.all

@microposts = params[:search].present?で値がsearchに値が入っているか(検索ボタンが押されているか)
値が入っていればmicropost_serachを動作させます。(中身は後ほどmodelに記載)値が入ってなければ、Micropost.allで全てのマイクロポストを表示させます。(そのあとは適宜ページネーション等に渡してください。)
ではmicropost_serachの中身です。

app/model/micropost.rb
def self.micropost_serach(search)
    Micropost.where(['title LIKE ?', "%#{search}%"])
end

whereメゾットとLIKE旬でマイクロポストの中からあいまい検索をします。

モデル名.where([カラム名前 LIKE ?, "検索したい文字列"])

検索したい文字列の両サイドにある「%」は任意の複数の文字列を表しています。
上記のは一つしかカラムを指定していませんが複数指定することもできます。

app/model/micropost.rb
def self.micropost_serach(search)
    Micropost.where(['title LIKE ? OR content LIKE ?', "%#{search}%", "%#{search}%"])
end

複数のカラムを指定する場合はカラム数に応じてORやANDで繋ぎ、検索したい文字列を増やしてあげると検索できます。

今回はさらに、マイクロポストにコメントされた文章も含めて検索してみます。
コメント機能についてこちら
https://qiita.com/E6YOteYPzmFGfOD/items/ef776d34908872ea19f7

app/model/micropost.rb
def self.micropost_serach(search)
    Micropost.includes(:comments).where(['microposts.title LIKE ? OR microposts.content LIKE ? OR comments.content LIKE ?',
    "%#{search}%", "%#{search}%", "%#{search}%"]).references(:comments)
end

これでコメント内も合わせて検索できるようになりました

#終わりに
最後までお読みいただきありがとうございました。検索文はいろいろパターンがあるので今後も新しい物を書いたときには記事にしてみようと思います。
ありがとうございました。

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

ツイッター風Railsアプリ最短復習(忙しい人の流し読みで開発シリーズ)

はじめに

こんにch… え?忙しい?? んじゃぁスタート!!!

具体的な手順

完成品GitHub

①アプリ立ち上げ

Terminal
$ cd Desktop
$ rails _5.2.4.1_ new cheaptweet -d mysql
$ cd cheaptweet
$ rails db:create
$ rails s
webBrowser
localhost:3000

rails.png

②テーブル作成

cheaptweet.jpeg

Gemfile
# 省略
gem 'devise'
Terminal
$ bundle install
$ rails g devise:install

control + c
$ rails s

$ rails g devise user
db/migrate/2020xxxxxxxxx_devise_create_users.rb
# 省略
 t.string :nickname,           null: false
# 省略
Terminal
$ rails db:migrate

aa.png

Terminal
$ rails g model tweet
db/migrate/2020xxxxxxxxxxxx_create_tweets.rb
# 省略
 t.string :text, null: false
 t.references :user, foreign_key: true, null: false
# 省略
Terminal
$ rails db:migrate

text.png

app/models/user.rb
#省略
 validates :nickname ,presence: true
 has_many :tweets
#省略
app/models/tweet.rb
#省略
 validates :text ,presence: true
 belongs_to :user
#省略

③会員登録・ログイン・ログアウトのみの基本循環構築

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
  end
end
Terminal
$ rails g devise:views
app/views/devise/registrations/new.html.erb
<!--省略-->
<div class="field">
  <%= f.label :nickname %><br />
  <%= f.text_field :nickname, autofocus: true %>
</div>
<!--省略-->
<!--  他の autofocus: true を削除 -->
Terminal
$ rails g controller tweets index
config/routes.rb
# get 'tweets/index'

#    |
#    v

resources :tweets, only: [:index]
root 'tweets#index'
#省略
app/views/layouts/application.html.erb
<!--省略-->
<body>
<!--↓追記↓----------------------------------------------->
  <header style="height: 50px; background-color: grey;">
    <% if user_signed_in? %>
      <%= current_user.nickname %>
      <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
    <% else %>
      <%= link_to "ログイン", new_user_session_path %>
      <%= link_to "会員登録", new_user_registration_path %>
    <% end %>
    <%= link_to "トップへ", root_path, style:"float: right;" %>
  </header>
<!--↑追記↑---------------------------------------------->
  <%= yield %>
</body>
<!--省略-->
app/views/tweets/index.html.erb
<div>※確認用</div>

main1.png main2.png
sign_up.png
login.png

④投稿(new→create)

config/routes.rb
# resources :tweets, only: [:index]

#    |
#    V

resources :tweets, only: [:index, :new, :create]
#省略
app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  def index
  end
####↓追記↓################################################
  def new
    @tweet = Tweet.new
  end
  def create
    @tweet = Tweet.new(tweet_params)
    if @tweet.save
      redirect_to root_path
    else
      render action: :new
    end
  end
  private
  def tweet_params
    params.require(:tweet).permit(:text).merge(user_id: current_user.id)
  end
####↑追記↑#################################################
end
new.html.erb
<%= form_with(model: @tweet, local:true) do |f| %>
  <%= f.text_area :text %>
  <%= f.submit '投稿' %>
<% end %>
app/views/layouts/application.html.erb
<!--省略-->
<body>
  <header style="height: 50px; background-color: grey;">
    <% if user_signed_in? %>
      <%= current_user.nickname %>
<!--↓追記↓----------------------------------->
      <%= link_to "投稿", new_tweet_path %>
<!--↑追記↑----------------------------------->
      <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
    <% else %>
      <%= link_to "ログイン", new_user_session_path %>
      <%= link_to "会員登録", new_user_registration_path %>
    <% end %>
  </header>
  <%= yield %>
</body>
<!--省略-->

hellob.png
hello.png

⑤一覧(index)

app/controllers/tweets_controller.rb
#省略
def index
####↓追記↓#####################################################
  @tweets = Tweet.all.includes(:user).order("created_at DESC")
####↑追記↑#####################################################
end
#省略
app/views/tweets/index.html.erb
× <div>※確認用</div>

<!--     |     -->
<!--     V     -->

<% @tweets.each do |t| %>
  <div><span style="color: red;"><%= t.user.nickname %></span><%= t.text %></div>
<% end %>

index.png
includesなし              includesあり
back1.png back2.png

⑥詳細(show)・編集(edit→update)・削除(destroy)

config/routes.rb
# resources :tweets, only: [:index, :new, :create]

#    |
#    V

resources :tweets
#省略
app/controllers/tweets_controller.rb
#省略
def show
  @tweet = Tweet.find(params[:id])
end
#省略
app/views/tweets/index.html.erb
×  <div><span style="color: red;"><%= t.user.nickname %></span><%= t.text %></div>

<!--    |    -->
<!--    V    -->

<div><%= link_to tweet_path(t.id) do %><span style="color: red;"><%= t.user.nickname %></span><%= t.text %><% end %></div>
app/views/tweets/show.html.erb
<div><span style="color: red;"><%= @tweet.user.nickname %></span><%= @tweet.text %></div>
<%= link_to "編集",  edit_tweet_path(@tweet.id) %><%= link_to "削除",  tweet_path(@tweet.id),method: :delete  %>
app/controllers/tweets_controller.rb
#省略
def edit
  @tweet = Tweet.find(params[:id])
end
def update
  @tweet = Tweet.find(params[:id])
  if @tweet.update(tweet_params)
    redirect_to tweet_path(params[:id])
  else
    render action: :edit
  end
end
def destroy
  @tweet = Tweet.find(params[:id])
  @tweet.delete
  redirect_to root_path
end
#省略
app/views/tweets/edit.html.erb
<%= form_with(model: @tweet, local:true) do |f| %>
  <%= f.text_area :text %>
  <%= f.submit '投稿' %>
<% end %>

削除後確認画面が欲しければ、

app/controllers/tweets_controller.rb
#省略
def destroy
  @tweet = Tweet.find(params[:id])
  @tweet.delete
#  redirect_to root_path
end
#省略
app/views/tweets/destroy.html.erb
<div>削除しました</div>
<%= link_to "トップに戻る", root_path %>

⑦編集・削除の権限設定

app/views/tweets/show.html.erb
× <%= link_to "編集",  edit_tweet_path(@tweet.id) %><%= link_to "削除",  tweet_path(@tweet.id),method: :delete %>

<!--    |    -->
<!--    V    -->

<% if user_signed_in? && current_user.id == @tweet.user_id %>
  <%= link_to "編集",  edit_tweet_path(@tweet.id) %><%= link_to "削除",  tweet_path(@tweet.id),method: :delete %>
<% end %>
app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
####↓追記↓###############################################
  before_action :unless_signin, only: [:new, :create,]
  before_action :unless_mytweet, only: [:edit, :update, :destroy]
####↑追記↑###############################################

#省略

  private

#省略

####↓追記↓###############################################
  def unless_signin
    redirect_to tweets_path unless user_signed_in?
  end
  def unless_mytweet
    redirect_to tweets_path unless user_signed_in? && current_user.id == Tweet.find(params[:id]).user.id
  end
####↑追記↑###############################################
end

⑧一覧ページネーション

Gemfile
#省略
gem 'kaminari'
Terminal
$ bundle install
Terminal
control + c
$ rails s
# @tweets = Tweet.all.includes(:user).order("created_at DESC")

#    |
#    V

@tweets = Tweet.includes(:user).order("created_at DESC").page(params[:page]).per(5)
app/views/tweets/index.html.erb
<!--省略-->
<%= paginate(@tweets) %>

⑨コメント機能

cheaptweet (2).jpeg

Terminal
$ rails g model comment
db/migrate/2020xxxxxxxx_create_comments.rb
#省略
  t.string :text, null: false
  t.references :user, foreign_key: true, null: false
  t.references :tweet, foreign_key: true, null: false
#省略
Terminal
$ rails db:migrate

comment.png

app/models/user.rb
#省略
 has_many :comments
#省略
app/models/tweet.rb
#省略
 has_many :comments
#省略
app/models/comment.rb
#省略
 validates :text, presence: true
 belongs_to :user
 belongs_to :tweet
#省略
config/routes.rb
# resources :tweets

#    |
#    V

resources :tweets do
  resources :comments, only: :create
end
app/controllers/tweets_controller.rb
#省略
def show
  @tweet = Tweet.find(params[:id])
####↓追記↓#############################################
  @comment = Comment.new
  @comments = Comment.where(tweet_id: params[:id]).order("created_at DESC").page(params[:page]).per(5)
####↑追記↑#############################################
end
#省略
Terminal
$ rails g controller comments
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    redirect_to tweets_path unless user_signed_in?
    @comment = Comment.new(params_comment)
    @comment.save
    redirect_to tweet_path(params[:tweet_id])
  end
  private
  def params_comment
    params.require(:comment).permit(:text).merge(user_id: current_user.id, tweet_id: params[:tweet_id])
  end
end
app/views/tweets/show.html.erb
<!--省略-->
<% if user_signed_in? %>
  <%= form_with(model: [@tweet, @comment], local: true) do |f| %>
    <%= f.text_area :text %>
    <%= f.submit 'コメント'%>
  <% end %>
<% end %>
<% if @comments %>
  <% @comments.each do |c| %>
    <div><span style="color: blue;"><%= c.user.nickname %></span><%= c.text %></div>
  <% end %>
  <%= paginate(@comments) %>
<% end %>

come.png

⑩ユーザー投稿一覧

Terminal
$ rails g controller users show
config/routes.rb
# devise_for :usersより下に
#省略
resources :users, only: :show
#省略
app/controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    @tweets = Tweet.where(user_id: params[:id]).order("created_at DESC").page(params[:page]).per(5)
    @nickname = User.find(params[:id]).nickname
  end
end
app/views/layouts/application.html.erb
× <%= current_user.nickname %>

<!--      |       -->
<!--      V       -->

<%= link_to user_path(current_user.id) do %><%= current_user.nickname %><% end %>
app/views/tweets/index.html.erb
× <div><%= link_to tweet_path(t.id) do %><span style="color: red;"><%= t.user.nickname %></span><%= t.text %><% end %></div>

<!--      |       -->
<!--      V       -->

<div><%= link_to user_path(t.user.id),style:"color: red;" do %><%= t.user.nickname %><% end %><%= link_to tweet_path(t.id) do %><%= t.text %><% end %></div>
app/views/tweets/show.html.erb
× <div><span style="color: red;"><%= @tweet.user.nickname %></span><%= @tweet.text %></div>

<!--      |       -->
<!--      V       -->

<div><%= link_to user_path(@tweet.user.id),style:"color: red;" do %><%= @tweet.user.nickname %><% end %><%= @tweet.text %></div>
app/views/users/show.html.erb
<p><%= @nickname %>の投稿一覧</p>
<% @tweets.each do |t| %>
  <div><%= link_to tweet_path(t.id) do %><%= t.text %><% end %></div>
<% end %>
<%= paginate(@tweets) %>

anna.png

⑪検索機能

Terminal
rails g controller tweets::searches
config/routes.rb
#省略
 namespace :tweets do
   resources :searches, only: :index
 end
#省略
# resources :tweets do より上に
app/models/tweet.rb
#省略
  def self.search(search)
    if search
      Tweet.where('text LIKE(?)', "%#{search}%").includes(:user)
    else
      Tweet.all.includes(:user)
    end
  end
#省略
app/controllers/tweets/searches_controller.rb
class Tweets::SearchesController < ApplicationController
  def index
    @tweets = Tweet.search(params[:keyword]).order("created_at DESC").page(params[:page]).per(5)
  end
end
app/views/tweets/index.html.erb
<%= form_with(url: tweets_searches_path, local: true, method: :get) do |f| %>
  <%= f.text_field :keyword %>
  <%= f.submit "検索" %>
<% end %>
<!-- 省略 -->
app/views/tweets/searches/index.html.erb
<%= form_with(url: tweets_searches_path, local: true, method: :get) do |f| %>
  <%= f.text_field :keyword %>
  <%= f.submit "検索" %>
<% end %>
<% @tweets.each do |t| %>
  <div><%= link_to user_path(t.user.id),style:"color: red;" do %><%= t.user.nickname %><% end %><%= link_to tweet_path(t.id) do %><%= t.text %><% end %></div>
<% end %>
<%= paginate(@tweets) %>

まとめ

網羅的でいい教材ですね。

これで難しい場合は以下をまわってみてください。

超最低限のRailsアプリを丁寧に作る(もう一度きちんと復習して初心者を卒業しよう)
『メッセージを投稿』できる最低限のRailsアプリを丁寧に作る(これで初心者完全卒業!)

次のレベルに行きたければ以下に行ってみてください。

『メッセージと複数画像の投稿』ができる最低限のRailsアプリを丁寧に作る
『2ページ遷移して会員登録』できる最低限のRailsアプリを丁寧に作る(deviseをウィザード形式に拡張)
『非同期でのメッセージ投稿』が理解できる最低限のRailsアプリを丁寧に作る(Ajax苦手の自分とお別れしよう)

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

「Rails」「初心者向け」ダイレクトメッセージ機能を1行ずつ解説

初めに

この記事は、Railsでダイレクトメッセージ機能を実装する方法について紹介します。
私自身が実装するにあたり、参考にさせて頂いた記事があります。
したがって、この記事の大部分は、それらの記事の内容に依っていますが、一部、私自身のしたいことがあり、応用した部分もあります。

この記事は、Railsチュートリアルを終えたレベルの初心者向けの記事です。
したがって、中級者以上の方が読まれると、冗長な文章に感じられると思います。ただ、初心者にとっては、当然の前提とされていることが、解説されていないことで詰まることは多いので、なるべく詳しく書こうと考えています。

参考文献
https://qiita.com/nojinoji/items/2b3f8309a31cc6d88d03
https://iberiko665.hatenablog.com/entry/2019/03/03/215730

ダイレクトメッセージとは...

そもそも、私はダイレクトメッセージが、投稿やチャットとどのように異なるのか理解していなかったので、一応整理しておきます。私としては以下のように分類しています。

  1. 投稿
    誰でも参加できる。誰でも投稿された文章、画像を見ることが出来る。
  2. ダイレクトメッセージ
    基本的に一対一や少数で他から見えない形で部屋が作られ、コメント出来る。
  3. チャット
    通常のHTTP通信と違い、web socketを使用している。 大きな違いとして、ユーザーがリロードせずに保存されたデータが表示される仕組みにできる(リアルタイム性がある)。

実装例

商品詳細画面(products/show)から「チャットを始める」ボタンを押すと、新たにルーム(rooms/show)が作られます。そのルームで、一対一でコミュニケーションを取ることが出来ます(「チャット」としていますが、上記の定義から言えば、「ダイレクトメッセージ」となります)。
app/views/products/show.html.erb
スクリーンショット 2020-03-18 7.26.08.png

app/views/rooms/show.html.erb
スクリーンショット 2020-03-18 7.43.29.png

注意

この画像はあくまでイメージです。
添付の画像は、私が制作したアプリの一部をスクリーンショットしたものです。
今回、添付の画像のようなデザイン(CSSやdiv要素)についての部分のコードは載せず、解説はしていません。
また、ゼロから始めるスタイルを採らず、重要な箇所だけを重点的に解説するスタイルを採ります。
書いていて、あまりに長大になったため、そこまで時間が取れませんでした。

ダイレクトメッセージ機能を、どのように実装するのか、何となくイメージを掴んでいただくことを目的としています。

モデリング

Productモデル、Userモデル、Roomモデル、Membershipモデル、Messageモデルの5つを使用します。
それぞれの関係性は以下の通りです。
Userモデル
実際にメッセージを送る主体。
Product(商品)を出品し、Room(ルーム)でMessage(メッセージ)を送り合います。

Productモデル
ユーザーによって出品された、商品を指します。
メッセージのやり取りは、「その商品」についてコミュニケーションを取りたい時に行うことが想定されています。

Roomモデル
各商品について、ユーザー間の関係性に応じて新たに作られるページを指します。
例えば、ユーザーAとユーザーBがメッセージを交わすために、一つのルームが作られますし、また、ユーザーAとユーザーCがメッセージを交わそうと思えば、別の新たなルームが作られます。

Membershipモデル
Roomモデルにおける関係性を指します。
具体的には、新たにルームが作られると、同一のroom_idを持つ、二つのMembershipモデルが作成されます。
実際のコードを見ながらの方が理解しやすいと思います。

Messageモデル
Roomモデルによって作られたルームで、ユーザー同士で交わされるメッセージを指します。

関連

あらかじめ、Railsチュートリアルで構築した認証機構や、sorcery, deviseといったgemを使用して、「現在ログインしているユーザー」を表すcurrent_userを作成していることが前提となります。
また、前述のモデルを作成していることが前提となります。

関連は以下の通りです。

user.rb
has_many :products
has_many :messages, dependent: :destroy
has_many :memberships, dependent: :destroy
product.rb
belongs_to :user
has_one :room
room.rb
has_many :messages, dependent: :destroy
has_many :memberships, dependent: :destroy
belongs_to :product
membership.rb
belongs_to :user
belongs_to :room
message.rb
belongs_to :user
belongs_to :room

カラムについては、確実に必要なのは、Messageモデルのcontentカラムです。
また、関連のためのforeign_keyも作成する必要があります。
foreign_keyがピンと来ない方は、下記の記事が分かりやすいです。
https://qiita.com/kazukimatsumoto/items/14bdff681ec5ddac26d1

本記事のモデリングでは、Productモデルは、user_idを、Roomモデルは、product_idを、Membershipモデル及びMessageモデルは、user_id, room_idをforeign_keyのカラムとして持たせる必要があります。

全体像の把握

データがどのように渡っていくか、この実装の骨子は以下の通りです。
productsコントローラーshowアクション =>
products showテンプレート =>
roomsコントローラーcreateアクション =>
roomsコントローラーshowアクション =>
rooms showテンプレート =>
messagesコントローラーcreateアクション =>

productsコントローラーshowアクション =>
products showテンプレート =>
(roomsコントローラーshowアクション)

以下、一つずつ解説していきます。

products#show - すでにルームを作成しているかのチェックを行う

products_contrller.rb
def show
 @product = Product.find(params[:id])
 @user = @product.user
  if current_user
    @current_user_memberships = Membership.where(user_id: current_user.id)
    @current_user_memberships.each do |current_user_membership|
      if current_user_membership.room.product_id == @product.id
        @has_room = true
        @room_id = current_user_membership.room_id
      else
        @room = Room.new
        @membership = Membership.new
      end
    end
  end
 end

1行ずつ解説していきます。
@product = Product.find(pramas[:id])
商品詳細ページなので、その商品のidをfindメソッドで取得して、@productという変数に代入しています。
その商品に紐づいたルームを作るために必要です。チュートリアルでもおなじみの記述だと思います。

@user = @product.user
これは、「その商品を出品したユーザー」を取得したいので、記述しています。products/showテンプレートで使用しています。
has_many, belongs_toなどで関連を作っていると、このように「モデル.モデル」とすることで、そのモデルと紐づいた別のモデルのデータを取得することが出来ます。

if current_user
これは、「現在ログインしているユーザーならば」、以下のif文に進むという意味です。
もし、ログインしていないならば、このshowアクションの場合、そのまま終わります。私の設計では、ダイレクトメッセージはログインしているユーザーだけに限定しています。
ログインしているユーザーに限定する理由は、一度でもルームを作成したことがあるかどうかを判断するために必要だからです。
詳しくは後の行で説明します。

@current_user_memberships = Membership.where(user_id: current_user.id)
この行で、行なっていることは、Membershipモデルから、現在ログインしているユーザーが保存されている関係性があるかをwhereメソッドで検索して、もしあれば、変数に代入しています。
なぜ、そのようなことをしているのかというと、一度でもルームを作成したことがあるかどうかを確かめ、もしあるならば、作成してあるルームへ、作成したことがないのなら、新たに作成するという条件分岐を記述するためです。
まずは、当然、作成されていないので、この行は考える必要はありません。
一通り見た後でないと、この行の意味は分からず、混乱すると思うので、今は理解できなくて結構です。
最初は、Membershipモデルにはデータが存在しないので、この変数には[](空の配列)が入ります。

@current_user_memberships.each do |current_user_membership|
この行からeach文で繰り返しを行なっています。前の行と同様に今は深く考える必要はないです。
実際の値としては、前の行の[]がそのまま、current_user_membershipという変数に入る、と考えてください。

if current_user_membership.room.product_id == @product.id
この行で場合分けを行います。具体的には、現在ログインしているユーザーの関係性を持つルームのproduct_idと、商品詳細ページのその商品のidが等しいかどうかで場合分けを行なっています。
等しいならば、trueとして@has_room..の行に進みますが、
今はcurrent_user_membership.room.product_idに値がないので、falseです。
else以下に進みます。

@room = Room.new
この行で、Roomモデルのオブジェクトを作成しています。オブジェクトといっても分かりづらいでしょうが、全ての値がnilのモデルが作成されているはずです。以下の画像のように作成されます。
今、@roomには、その作成しただけの値の何も入っていない型を代入しています。
Room.new
スクリーンショット 2020-03-18 8.55.26.png

@membership = Membership.new
この行も同様です。

products/show.html.erb

show.html.erb
<div class="chatBox">
  <% if current_user %>
    <% if @has_room %>
      <div class="already-has-chatting">
        <%= button_to "チャットを再開する", room_path(@room_id), method: :get, class: "restart-chat-button" %>
      </div>
    <% else %>
      <div class="start-chat">
        <%= form_with url: rooms_path, method: :post, local: true do |f| %>
          <%= hidden_field_tag :product_id, @product.id %>
          <%= hidden_field_tag :user_id, @user.id %>
          <%= f.submit "チャットを始める", class: "start-chat-button" %>
        <% end %>
      </div>
    <% end %>
  <% end %>
</div>

product/show.html.erbでしていること
すでにルームを作成したことがあるならば、そのルームのページを開きたいので、GETメソッドを/room/:idに送信する。
まだ、ルームを作成したことがないならば、ルームを作るため、form_withを使い、POSTメソッドを/roomsに送信する、ということです。

以下、説明していきます。
<% if current_user %>
この行は、先のコントローラーと同様に、現在ログインしているユーザーを想定してダイレクトメッセージ機能を実装しているので、記述しています。

<% if @has_room %>
この行は、products#showで、もしすでに作成したルームがあるならば、showアクションで、trueという値を@has_roomという変数に代入しているのですが、現時点では作成していないので、falseです。
したがって、このif文はelseに進みます。
今は深く考える必要はないです。

<%= form_with url: rooms_path, method: :post, local: true do |f| %>
このform_withは、Products#showでの値を、次のRoomsコントローラーへ渡すことが目的です。
Railsチュートリアルでは、form_for(@user)のような使用がメインだったかと思いますが、現在、form_withを使用することが推奨されているため、form_withを使用していきましょう。

form_withでは、urllocalを指定する必要があります。urlは送信先の名前付きルートを、methodはGETかPOSTか、DELETEかといった種類を指定するモノです。localというのは、Ajaxという機能を使用するのかを指定するもので、今回は使用しないので、local: trueとします。

<%= hidden_field_tag :product_id, @product.id %>
この行は、products#showで取得した商品のidを次のroomsコントローラーへ引き継ぎたいので、記述しています。

<%= hidden_field_tag :user_id, @user.id %>
この行も同様に、@user = @product.userで取得していた@userの値を引き継ぎたいので、記述しています。

<%= f.submit "チャットを始める", class: "start-chat-button" %>
この行は、ボタンが押されると、hidden_fieldで渡した値がroomsコントローラーへ行くように指示しています。


hidden_fieldについて
hidden_fieldとhidden_field_tagの違いや、使い方について補足したいと思います。
それは、私自身がダイレクトメッセージ機能を実装するまで、両者の違いをうまく理解できていなかったと思うからです。
まず先ほど解説したフォームでは、hidden_field_tagを使用しましたが、
その理由は「モデルに紐づく形で値を渡す必要がないから」です。

文章で書いても分かりづらいと思うので、実際の値を見てみましょう。
まず、hidden_field_tagで渡したこのproducts/show.html.erbでのparams(パラメータ)は以下の通りです。
スクリーンショット 2020-03-18 10.26.16.png

全体像の把握の章でも説明したように、このチャットを始めるボタンを押した際、送信先はroomsコントローラーのcreateアクション(rooms#create)です。
したがって、値を見るためにrooms#createにてbinding.pryで確かめています
(binding.pryはデバック方法の一つです)。

この赤字のパラメータの内容が、今、取り上げたい箇所です。
全ての値が一つの{}(ハッシュ)の中にKey, Value(product_idというKeyと、3というValueなど)という形式で格納されていることがお分かりいただけると思います。
つまり、一緒くたにされているということです。
これが、hidden_field_tagを使用した際の、値の渡り方です。

次に、hidden_fieldを使用した場合には、どのように値が渡るのか確かめてみましょう。

<%= form_with model: product, url: rooms_path, method: :post, local: true do |f| %>
   <%= f.hidden_field :product_id, value: product.id %>
   <%= f.hidden_field :user_id, value: user.id %>
   <%= f.submit "チャットを始める", class: "start-chat-button" %>
<% end %>

先ほど解説したform_withの引数にmodel: productとしている点、f.hidden_fieldhidden_field_tagであった部分を変更している点が異なります。

このように変えた場合、どのように値が渡るのかというと、以下のように渡ります。
スクリーンショット 2020-03-18 10.37.17.png

注目していただきたいのは、"product"=>{ "product_id"=>"1", user_id"=>"1"}となっている部分です。
このようにhidden_fieldを使用すると、modelとして指定した値の内部にhidden_fieldで指定した内容が格納されます。そのため、f.hidden_fieldform_withのフォームビルダーをレシーバとして記述する必要があります。
つまり、(product)モデルに紐づく形式で値が渡っているということです。

-補足-
フォームビルダーとは、form_withなどdo-endで囲まれた部分をブロックと呼びますが、そのブロック内部で|f|と設定しているもののことです。
レシーバとは、User.findのような記述でのUserに当たるモノです。findというメソッドを受ける対象と考えると分かりやすいです。

結論として、今回のproducts/show.html.erbからrooms#createへはモデルに紐づく形で値を渡す必要がなかったので、hidden_field_tagを使用しています。

さらに、これは余談ですが、hidden_fieldhidden_field_tagも第一引数は「どのように値を受け取りたいか」、第二引数は「渡す値」を指定します。
私は、これまでform_withf.text_fieldなどでも、テーブルのカラムを渡すものと思っていたのですが、これは勘違いでした...。
例えば、Railsチュートリアルでは、User登録の際に

form_for(@user) do |f|
 f.text_field :name
 f.submit
end

このような、記述で値を更新したと思いますが、てっきり、Userモデルを作成した際に、nameカラムも作成したので(rails g model user name:string ..のこと)、そのカラム名をf.text_field :nameとして指定しなければならないと思っていました(https://railstutorial.jp/chapters/sign_up?version=5.1#sec-using_form_for)。

これは、別にf.text_field :happyでもなんでも良いです。その際に、値を受け取る方で、params[:happy]として受け取れば良いだけです。
「値をどのように受け取るか」を指定しているだけなので、自由に決めて良いです。
もし私と同じように勘違いされている方がいらっしゃれば、色々binding.pryで試してみられると良いかと思います。

rooms#create - 一つのRoomモデル、二つのMembershipモデルの作成・保存

さて、products/show.html.erbから渡された値は、roomsコントローラーのcreateアクションに送信しているので、その部分の説明をします。

rooms_controller.rb
def create
    @room = Room.new
    @room.product_id = params[:product_id]
    @room.memberships.build(user_id: params[:user_id])
    @room.memberships.build(user_id: current_user.id)
    @room.save
    redirect_to @room
end

rooms#createでしていること
その商品に紐づいたルームを作りたいので、Roomモデルに先の引き継いだproduct_idの値を代入していること、また、そのルームに紐づく関係性(Membershipモデルを指す)を二つ作成していることです。
関係性という考え方が難しいと思いますが、じっくり追っていけば理解できると思うので、頑張りましょう。

@room = Room.new
この行では、改めてRoomモデルの型を作成しています。
ただ、作成しただけなので、ここであまり考えすぎないようにしてください。
作成したRoomモデルにそれ以降の行で、データを登録していきます。

@room.product_id = params[:product_id]
この行では、先のhidden_field_tagで引き継いだ、商品詳細ページの「その商品」のidを取得しています。
params[:product_id]とすることで、先ほど紹介したパラメータの中で、product_idというKeyに対応するValueを取得できます。
その値を@room.product_idに代入しています。Roomモデルのproduct_idカラムに代入しているということになります。

スクリーンショット 2020-03-18 11.55.49.png

コードの出力から見ると、@roomにはproduct_idというKeyだけ値が入っています。
idの6という数字は、スクショを取るためのこちらの都合なので、気にしないで良いです。

@room.memberships.build(user_id: params[:user_id])
この行も、引き継いだユーザーのidをparams[:user_id]として受け取り、membershipsモデルのuser_idカラムに代入しています。
products/show.html.erbから渡されたuser_idは、「その商品を出品したユーザー」のidです。
次の行の@room.memberships.build(user_id: current_user.id)は今操作している「現在ログインしているユーザー」のidです。

この2行でしていることは、それぞれのparams[:user_id]と、current_user.idをMembershipモデルのuser_idとして登録し、さらにMembershipモデルのroom_idはこのrooms#createの最初の行で作成したRoomモデルを代入しています。

そうすることで、同じルームのidを持ったuser_idが異なるMembershipモデルが二つ作られます。
ここで、作成したMembershipモデルを、今は気にしないで良いですと言った最初の、products#showにて、使用することで、すでにルームを一度でも作成したことがあるのかを確かめます。

この部分は、ダイレクトメッセージ機能の中でも難しい部分なので、自分なりに考えてみて下さい。

@room.memberships.buildという記述は、RoomモデルとMembershipsモデルに関連がある場合、「そのRoomモデルに紐づくMembershipモデルの型を作成する」という意味です。
MembershipモデルはRoomモデルに従属する関係なので、「そのRoomモデルに紐づく」という部分を加えて、記述する必要があります。

@room.save
この行は、Roomモデルと、Membershipモデルで、代入してきた値を保存しています。

redirect_to @room
rooms#showへリダイレクトさせています。この記述の仕方は、下記の記事が詳しいです。
https://qiita.com/Kawanji01/items/96fff507ed2f75403ecb


rooms#show - ルームのこれまでのメッセージの取得及びMessageモデルの作成

rooms_controller.rb
def show
  @room = Room.find(params[:id])
  if Membership.where(user_id: current_user.id, room_id: @room.id).present?
    @messages = @room.messages
    @message = Message.new
  else
    redirect_back(fallback_location: root_path)
  end
end

rooms#showでしていること
1. rooms#createで作成したRoomモデルに紐づいたmessagesがすでにあるなら、それを取得すること。
2. 新しくメッセージをするので、Messageモデルを作成する、ということです。

以下、1行ずつ解説していきます。

@room = Room.find(params[:id])
この行は、rooms#createで、@room.saveとした時に、登録されたRoomモデルのidを取得することで、「そのルーム」という部分を検索しています。

if Membership.where(user_id: current_user.id, room_id: @room.id).present?
この行は、先のrooms#createで登録したMembershipモデルをwhere文を使うことで、検索しています。
そして.present?メソッドが続くので、「もしあるならば」進みなさい、という意味になります。

つまり、保存された全てのMembershipモデルを参照して
1. user_idが「現在ログインしているユーザー」のモノで、
2. room_idが、今findで取得したルームのidのroom_id
というMembershipモデルがあるか、検索しています。

この行は、ルームを作成していないユーザーがそのルームに進入することを防ぐ役割があります。

where文の使い方は、多くの記事がありますし、関連するモデルを検索すると言った場合でない限り、さほど難しくないと思うので、簡単に説明して終えようと思います。
where文は、User.where(name: "nanasi")のように記述し、役割としては、今まで説明してきたように、その「モデルのデータを検索すること」です。findやfind_byに近い役割を持ちます。
文法は、モデル.where(カラム: 検索したい内容)です。
上の記述なら、Userモデルの中から、"nanasi"というnameのUserがいないか検索することが出来ます。

@messages = @room.messages
「そのルーム」でこれまで、作成されてきたメッセージを取得するために、記述しています。
@messagesという変数に代入しておいて、テンプレートでeach文を回すことで、これまでに保存されてきたメッセージを表示します。そのための布石です。

@message = Message.new
この行は、Messageモデルを作成しています。これまで何度も見てきたので、大丈夫だと思いますが、殻を作成しただけで、中身はありません。とりあえず、型だけを作成しておきます。

else文の方の
redirect_back(fallback_location: root_path)
は、root_pathへリダイレクトさせます。

rooms/show.html.erb - メッセージとフォームの表示

show.html.erb
<div class="container">
  <div class="row">
    <%= render "message_area", messages: @messages %>
  </div>
  <div class="row">
    <div class="col-md-8 offset-md-2" >
      <%= form_with model: @message, url: messages_path, method: :post, local: true, html: { class: "message-form"} do |f| %>
        <div class="input-message">
          <%= f.text_area :content, class: "content" %>
        </div>
        <%= f.hidden_field :room_id, value: @room.id %>
        <%= f.submit "+", class: "btn" %>
      <% end %>
    </div>
  </div>
</div>
_message_area.html.erb
<div class="col-md-8 offset-md-2">
  <div class="message-area">
    <% if messages.present? %>
      <% messages.each do |message| %>
      <div class="message-one">
        <% user = message.user %>
        <% class_suffix = current_user == user ? "right" : "left" %>
        <div class="user-icon-<%= class_suffix %>">
          <% if message.user.image.url(:small) %>
            <%= image_tag message.user.image.url(:small) %>
          <% else %>
            <%= message.user.name %>
          <% end %>
        </div>
        <div class="balloon-<%= class_suffix %>">
          <div class="balloon-content"><%= message.content %></div>
          <div class="timestamp">
            <%= time_ago_in_words(message.created_at) %>前
          </div>
        </div>
      </div>
      <% end %>
    <% end %>
  </div>
</div>

rooms/show.html.erbでしていること
1. 今までそのルームで行われてきた、メッセージを全て表示すること
2. 新たにメッセージを作成するために、フォームから値を渡すこと
です。

1行ずつ解説していきます。

<%= render "message_area", messages: @messages %>
この行は、メッセージの表示部分をパーシャルとして切り出したので、その記述です。
rooms#showで@messagesという変数に、「そのルームの」今までの全てのメッセージを取得してあるので、その変数を使用します。

まず、このパーシャル内部を先に説明します。
<% if messages.present? %>
この行で、これまでそのルームで投稿されたメッセージがあるかどうかをチェックしています。

<% messages.each do |message| %>
この行では、rooms#showで取得した@room.messagesをeach文で繰り返しています。
こうすることで、すでに投稿されたメッセージの内容を全て取得することができます。

<% user = message.user %>
この行は、「そのメッセージを投稿したユーザー」を取得しています。この形式で記述することでUserモデルを取得できることは、すでに説明したと思います。
このように、userという変数に代入する目的は自分と相手の投稿を判断して、相手の投稿であれば、右から吹き出しを出現させ、自分の投稿であれば、左から出現させるというデザイン上のものです。
したがって、特にこだわらないという場合は不要です。

<% class_suffix = current_user == user ? "right" : "left" %>
この行も同様にデザイン上の記述です。
現在ログインしているユーザーが前の行で記述した「そのメッセージを投稿したユーザー」ならば、右から、そうでないならば左から吹き出しが出るように設計しています。

<div class="balloon-content"><%= message.content %></div>
何行か飛ばした後、この行で、今まで保存されてきたメッセージを表示しています。

このパーシャルで、重要な部分はこのmessage.contentぐらいで、後は、ほとんどデザインの問題なので、説明を省いた部分が多いですが、デザイン部分に興味がある方は、私が参考にさせて頂いたサイトをご覧になって下さい(https://iberiko665.hatenablog.com/entry/2019/03/03/215730 )。

さて、パーシャルが終わったので、
次は、この行です。
<%= form_with model: @message, url: messages_path, method: :post, local: true, html: { class: "message-form"} do |f| %
フォームを作成しています。このフォームにより、新規のメッセージを投稿できるようにしています。
html:というシンボルの部分は、ハッシュを渡していますが、これもclassを定義することで、CSSを当てるためなので、重要ではありません。

このフォームの送信先は、messages#createです。
また、前回のform_withと異なり、modelというシンボルを設定しています。
これは後の行で「messageモデルの値を更新したいので」、このように記述しています。
modelに紐づく形で、値を更新したい場合は、このようにmodelを設定する必要があります。

<%= f.text_area :content, class: "content" %>
この行は、ユーザーにメッセージを記述してもらう部分をtext_areaを使用して、作成しています。

<%= f.hidden_field :room_id, value: @room.id %>
この行は、先に説明したhidden_fieldを使用しています。
Messageモデルに紐づいた形で、値を渡したいので、このように記述しています。
hidden_fieldとして渡している値は、@room.idです。
「そのルームの」メッセージとして保存したいので、渡す必要があります。
この行、そしてこのフォームについては、最後のmessages#createとも密接に関わっているので、以上の説明だけではなく、messages#createの説明と合わせて読んでいただくことで理解できるかと思います。

rooms/show.html.erbの説明は以上です。

messages#create - 新規メッセージの作成・保存

messages_controller.rb
class MessagesController < ApplicationController
  def create
    if Membership.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
      @message = Message.create(message_params)
      redirect_to room_path(@message.room_id)
    else
      redirect_back(fallback_location: root_path)
    end
  end

  def message_params
    params.require(:message).permit(:user_id, :room_id, :content).merge(user_id: current_user.id)
  end
end

messages#createでやっていること
このアクションでしていることは、シンプルで、投稿されたメッセージを保存することだけです。

1行ずつ解説していきます。

if Membership.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
この行は、Membershipモデルから、適切なuser_idとroom_idなのかどうかをチェックしています。
rooms#showでも、同様のif文があったので、大丈夫だと思います。

@message = Message.create(message_params)
この行で、投稿されたメッセージを保存しています。
createメソッドは、作成と保存を同時に行うことができるメソッドです。
つまり、rooms#createでは、Room.newとしてオブジェクトを作成し、最後に@room.saveとして保存しましたが、それを一気にします。
複雑なのは、ストロングパラメータでmergeメソッドが使用されている点です。

params.require(:message).permit(:user_id, :room_id, :content).merge(user_id: current_user.id)
ストロングパラメータ自体は、チュートリアルでもよく解説されていますし、params.require(:--).permit(:--,)という形式に抵抗はないと思いますが、その後のmergeとして続いている部分が私としては難しく、ストロングパラメータとmergeメソッドの組み合わせについてきちんと解説している記事を私自身が見つけられなかったこともあり、詳しく解説していきたいと思います。

まず、改めてですが、前回のform_withhidden_fieldを使用し、hidden_field_tagを使用しなかった理由から説明します。
それはMessageモデルに紐づく形で値を渡したかったからです。
つまり、値は
スクリーンショット 2020-03-18 20.39.48.png
このように、"message"=>{ "content"=>"Goodby", ... }といった形式で渡したい。
なぜなら、ストロングパラメータとして更新するにはそのように形式が整っていないと更新できないからです。
ここまでは、前回のhidden_fieldhidden_field_tagの違いをよく考えてみれば、分かると思います。

さらに、Messageモデルはcontent、user_id、room_idの三つのカラムのデータを更新する必要があります。
contentはメッセージの内容、user_idはどのユーザーが投稿したメッセージか、room_idはどのルームで投稿されたのか、という情報でMessageモデルには全て必須です。

しかし、スクショを見ると、"message"=>以下のハッシュにcontent, room_idはありますが、user_idがありません。したがって、このままでは、値が更新できず、
スクリーンショット 2020-03-18 20.50.14.png
このスクショのように、ROLLBACK、つまり何か不具合があったので、保存しませんというサインが出ています。

したがって、「user_idも同時に更新したいので、この値を入れてください」とお願いするために、mergeメソッドを使用します。
mergeメソッドの公式的な説明は「ハッシュの結合」です。
つまり、ストロングパラメータとしては、"message"=> { ... }というMessageモデルのハッシュに、user_idを強引にカットインさせるために使用します。
具体的には、
.merge(user_id: current_user.id)とすることで、そのハッシュに入れることが出来ます。
それはbinding.prymergeメソッドを記述した際、記述しなかった際を比較してみると分かると思います。

スクリーンショット 2020-03-18 20.57.37.png

このように、mergeメソッドを使用すると、パラメータの内部にうまく入れることが出来ました。

あとは、リダイレクトだけなので、説明は省きます。

products#show - 一度ルームを作成している場合

これから、すでにルームを作成し、再び同じルームに入る場合の処理について解説していきます。
もう一度該当のコードを載せます。

products_controller.rb
def show
 @product = Product.find(params[:id])
 @user = @product.user
  if current_user
    @current_user_memberships = Membership.where(user_id: current_user.id)
    @current_user_memberships.each do |current_user_membership|
      if current_user_membership.room.product_id == @product.id
        @has_room = true
        @room_id = current_user_membership.room_id
      else
        @room = Room.new
        @membership = Membership.new
      end
    end
  end
 end

rooms#createを思い出して頂きたいのですが、すでにルームを作成したことがあれば、Membershipモデルには、「現在ログインしているユーザー」のidが、user_idとして登録され、保存されています。

@room.memberships.build(user_id: current_user.id)
(rooms#create)

ここでは、whereメソッドを使い、Membershipモデルから、その関係性を全て取得しています。
全て取得しているというのは、ポイントの一つです。
それは、「現在ログインしているユーザー」は、他の商品のページで新たにルームを作成している場合も考えられるからです。
とりあえず、「現在ログインしているユーザー」が関わっているMembershipモデルを全て取得したい。
さらに言えば、「現在ログインしているユーザー」が関わっているroom_idを取得したい、ということです。
それを変数に代入します。

次に、その値をeach文で全て検討します。
if文のcurrent_user_membership.room.product_idという長いメソッドチェーンにより、「現在ログインしているユーザーをuser_idとしたMembershipモデルに紐づいたルームの商品のID」が取得できます。
メソッドチェーンとは、メソッドをつないでいったものです。

このメソッドチェーンは分かりづらいと思いますが、このように考えてみてください。
まず、現在ログインしているユーザーをuser_idとするMembershipモデルは多くある
=>
それら一つ一つのMembershipモデルが作成されるときに同時に作成したRoomモデルを探す
=>
そのRoomモデルを作成するときに、hidden_fieldで引き継いだproduct_idは、つまり「その商品」のページからボタンをクリックした「その商品のid」を指す
=>
したがって、 @product.idと等しい

このif文によって、現在ログインしているユーザーが一度でも、その商品について、あるルームを作成したことがあるかどうかがチェックされ、一度でもあるならばtrueとして進みます。

@has_room = true
この行では、前述のチェックのサインとして真偽値のtrueを変数に代入しています。
変数名に大きな意味はありません。
product/show.html.erbにて、条件式で使うため、設定しています。

@room_id = current_user_membership.room_id
この行は、先のif文で選抜された唯一のcurrent_user_membershipという変数から、room_idを取得し、代入しています。
@current_user_memberships = Membership.where(user_id: current_user.id)
序盤のこの行をみて分かるように、current_user_membershipという変数は、結局Membershipモデルだということを確認しておけば、難しくはないと思います。
この行の意味については、前の行と同じくshow.html.erbにて使用するためです。

product/show.html.erb - 一度ルームを作成している場合

product/show.html.erb
<div class="chatBox">
  <% if current_user %>
    <% if @has_room %>
      <div class="already-has-chatting">
        <%= button_to "チャットを再開する", room_path(@room_id), method: :get, class: "restart-chat-button" %>
      </div>
    <% else %>
      <div class="start-chat">
        <%= form_with url: rooms_path, method: :post, local: true do |f| %>
          <%= hidden_field_tag :product_id, @product.id %>
          <%= hidden_field_tag :user_id, @user.id %>
          <%= f.submit "チャットを始める", class: "start-chat-button" %>
        <% end %>
      </div>
    <% end %>
  <% end %>
</div>

<% if current_user %>
最初の行である、この行では、「現在ログインしているユーザー」であるか?をチェックしています。
この点は大丈夫だと思います。

<% if @has_room %>
この行は、showアクションでtrueを代入しているため、trueとして進みます。
このように、すでにルームを作成しているかどうかのチェックのために使用します。

<%= button_to "チャットを再開する", room_path(@room_id), method: :get, class: "restart-chat-button" %>
この行は、ボタンタグを利用したリンクです。
ルームを作成していなかった場合には、rooms#createへ行き、そこで、Roomモデル、Membershipモデルを作成し、rooms#showへリダイレクトさせました。
今回は、すでにルームがあり、そのidも知っている状態なので、room_path(@room_id)として、該当のルームへのリンクとしています。

@room.idではなく、@room_idである点に注意してください。
この@room_idはshowアクションで最後に値を代入して変数のことです。

rooms#showが、すでにルームを作成している場合、新たにルームを作成する場合の合流地点になるので、あとは同じです。

最後に

以上で、ダイレクトメッセージ機能の説明は終わりです。

もともと私がダイレクトメッセージ機能を知ったのは、@nojinojiさんの記事
https://qiita.com/nojinoji/items/2b3f8309a31cc6d88d03
からです。
多くのモデルを組み合わせることにより、決められたユーザー同士だけのルームを作り、別のユーザーなら新たなルームが作られる。写経し、1行ずつ意味を考えてみて、その精緻さに感動しました。

私は、ある商品に紐づく形でダイレクトメッセージ機能を実装したいと思っていました。
人それぞれ、実装したい形は異なっていると思うので、この記事がそうした応用に役立てばいいと思い、共有します。

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

RspecでProcess.fork内のメソッドを検証したい

fork内のメソッドコールを単純に検証するとうまくいかない

こんなクラスがあるときに

class Dog
  def crow(str)
    p str
  end

  def walk
    Process.fork do
      crow('bowbow!')
    end
  end
end

walkメソッド内でcrowメソッドが呼び出されていることを検証しようとしてこんなspecを書いてみるとうまく通りません

  let(:dog) { Dog.new }

  it {
    expect(dog).to receive(:crow).with('bowbow!').once
    dog.walk
  }
Failures:

  1) Dog is expected to receive crow("bowbow!") 1 time
     Failure/Error: expect(dog).to receive(:crow).with('bowbow!').once

       (#<Dog:0x00005571aad36820>).crow("bowbow!")
           expected: 1 time with arguments: ("bowbow!")
           received: 0 times

forkブロック内は別プロセスなので検証できないのですね。

結論

こういう場合はProcessをmock化してしまうのが手っ取り早いようです。

  let(:dog) { Dog.new }

  it {
    expect(Process).to receive(:fork) do |&block|
      expect(dog).to receive(:crow).with('bowbow!').once
      block.call
    end

    dog.walk
  }

expect_any_instance_of も利用できます

  let(:dog) { Dog.new }

  it {
    expect(Process).to receive(:fork) do |&block|
      expect_any_instance_of(Dog).to receive(:crow).with('bowbow!').once
      block.call
    end

    dog.walk
  }

引数の検証がちゃんとできているのか確認するためにあえて間違った引数を検証してみます。

  let(:dog) { Dog.new }

  it {
    expect(Process).to receive(:fork) do |&block|
      expect(dog).to receive(:crow).with('purrr').once
      block.call
    end

    dog.walk
  }
Failures:

  1) Dog is expected to receive crow("purrr") 1 time
     Failure/Error: crow('bowbow!')

       #<Dog:0x00005588d1a8ed78> received :crow with unexpected arguments
         expected: ("purrr")
              got: ("bowbow!")

ちゃんと検証してくれているようです。

参考

https://stackoverflow.com/a/6159391

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

[payjp]tokenは発行できているのに呼び出せるはずのデータがテーブルに入らないエラーの解決方法

1.どのような状態だったか

javascriptでpayjp-token取得はコンソールで確認すると全て成功しており、ページ遷移の部分でもエラーは出ず、validationに関するエラーも出ないのに、コントローラーでpayjp-tokenの値が取り出せないという状態でした。(エラー文が出ず、正常にnewアクションに戻るという症状でした。)

下記の二つの記事を参考にpay.jp機能を実装をしていました。(大元が上の方で、それの詳細まで記述されていたのが下記の方でした。)

https://qiita.com/takachan_coding/items/f7e70794b9ca03b559dd
https://qiita.com/emincoring/items/ce29dbbd182aa3c49c6b

なのでとりあえず動作を見るべく細部まで記述のあった下記のブログを参考に下記のようにviewを作成しました

※今回はviewの記述によるエラーだったので、その部分についてのみ記述します

app/view/cards/new.html.haml
.content__title
  %h2 クレジットカード情報入力
.content__credit-card
  .content__credit-card__inner
    = form_with url: cards_path, method: :post, html: { name: "inputForm" } do |f| -# createアクションのパスを指定
      = f.label :カード番号, class: 'label'
      %span 必須
      = f.text_field :card_number, type: 'text', class: 'input-number', placeholder: '半角数字のみ', maxlength: "16"
      .cards-expire
        = f.label :有効期限, class: 'label'
        %span 必須
        %br
        .cards-expire__wrap
          = f.select :exp_month, [["01",1],["02",2],["03",3],["04",4],["05",5],["06",6],["07",7],["08",8],["09",9],["10",10],["11",11],["12",12]],{} , class: 'input-expire'
          %span.expire-text%br
        .cards-expire__wrap
          = f.select :exp_year, [["19",2019],["20",2020],["21",2021],["22",2022],["23",2023],["24",2024],["25",2025],["26",2026],["27",2027],["28",2028],["29",2029]],{} , class: 'input-expire'
          %span.expire-text.cards-expire
        = f.label :セキュリティコード, class: 'label'
        %span 必須
        = f.text_field :cvc, type: 'text', class: 'input-number', placeholder: 'カード背面4桁もしくは3桁の番号', maxlength: "4"
      .content-bottom#card_token
        = f.submit '追加する', class: 'content-bottom--add-btn', id: 'token_submit'

2.原因

payjp-tokenの受け渡しを図解すると、
submitを押してjavascriptに入力情報送信→javascriptでpayjp-tokenに変換→form_withの実行でcontrollerにpayjp-token受け渡し
となります。
この時、数値の受け渡し毎にそれぞれidが1つずつ必要になるのですが、自分は1つしか用意していませんでした。
そのため、受け取りid=受け渡しidとなり、入力情報(カードの期日など)を素のままでform_withで受け取り、コントローラに値を返してしまっていたというのが原因でした。

3.解決方法

form_withにも新たにidを定義すれば希望通りの挙動をしました

app/views/cards/new
〜前略〜
= form_with url: pay_cards_path, method: :post, id: 'charge-form',html: { name: "inputForm" }do |f|
# idの名前はどのようなものでも大丈夫です。筆者はpayjpのホームページで記述のあったid名を引用しました。
〜後略〜

4.(補足)エラーの見つけ方

まず今回のように、railsのエラー文やjavascriptのエラー文(Google Chromeの検証のconsole上のもの)が出なければ数値の受け取りミスであることがほとんどだと思われます。

したがって筆者はまずコントローラでbinding.pryを挟んでどこまで数字が取れているか確認し、次にjavascriptにもconsole.log();を挟んで確認しました。結果、javascriptは問題ないということがわかり、コントローラとビューの確認を行いました。

その結果、javascriptには送れているから、そのあとの受け取りのタイミングで間違いがあると考え、submitの後に処理されるのはなにかと考えた時、残るのはform_withだけなのでその記述に関する資料を検索していたところ、このメソッドもid指定できることがわかり解決に至りました。

このようにエラー文が出なくて困ったら、デバックのコードを1行ずつ挟んで確認すれば原因を特定しやすくなり、エラーを見つけることができます。

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

【rails】partialをサクっと実装してちょっと覚える

partialは部分テンプレートを使用して、同じパターンの塊を複数生成するのに便利なものです。
eachでもできますが、処理速度がpartialの方が早いらしいですよ。

そんなpartialですが、理解するのに苦戦した記憶があります。
partialを知らなかった頃の自分に読ませたい記事をかけたらなと思います。

使うもの
・Ruby on Rails
・mysql
・haml

$ rails sでハローワールドを表示できる環境を整えてください。

作れるもの

d6941f061beebf513046f5abd5ff8a48.gif
こんな感じのものをpartialで実装します。

[1]controllerを生成する

Tシャツの画像リンクを表示させるプチ実装をして覚えましょうという記事です。
まずはcontrollerを生成します。
$ rails g controller tshirts indexをターミナルで入力し、
tshirts controller と、indexアクションを一緒に生成します。

[2]とりあえずrootをtshirts#indexにしときましょ

# routes.rb
Rails.application.routes.draw do
  root to: 'tshits#index'
end

今回はサクッと実装なので、必要なアクションはこれだけです。

[3]フロントをコピペで実装しときましょ

下記のコードを該当ファイルに書いちゃいましょ。

app/views/tshrts/index.html.haml
%ul
  -# ~~~ ここから ~~~
  %li
    = link_to "https://www.ttrinity.jp/product/5734710", target: :_blank do
      = image_tag "http://okachanblog.com/static/20200315tshirt_list/tshirt_1.jpg", width: '100%'
      %br/
      %span Tシャツの名前
  -# ~~~ ここまで ~~~

上記の「ここから 〜 ここまで」を増やすと、パネルが並びます。
この部分が繰り返し表示されるので、なんとなく覚えておいてください。
target: :_blankは、別タブでサイトを表示させるオプションです。

app/assets/stylesheets/tshits.scss
body,ul{
  margin: 0;
  padding: 0;
}

ul{
  margin: 50px auto;
  width: 80%;
  display: flex;
  flex-wrap: wrap;
}

li{
  list-style: none;
  width: 17%;
  border: 1px #000 solid;
  text-align: center;
  margin: 1%;
}

li a:hover{
  opacity: 0.5;
}

b4c25ce9b91bb1bd328a7813bf8ddb26.gif

どうでしょうか、ここまでは表示できましたか?

[4]modelを作っちゃいましょ

ターミナルで、$ rails g model tshirtとマイグレート ファイルを生成しましょう。

20200XXXXXXXX_create_tshirts.rb
class CreateTshirts < ActiveRecord::Migration[6.0]
  def change
    create_table :tshirts do |t|
      t.string :title
      t.text :image
      t.text :url
      t.timestamps
    end
  end
end

上記のようにtitle、image、urlを追加して$ rake db:migrateします。
どうでしょう。テーブルは作成できましたか?

[5]作成したテーブルに、手動で情報を入力しちゃいましょう。

面倒だと思う方はコンソールから!コンソールがよくわからない方は、手動で入力しましょう。
下記をコピペでテーブルに情報の流し込みをしましょう。

id title image url created_at updated_at
1 獲物を狙うトラ http://okachanblog.com/static/20200315tshirt_list/tshirt_1.jpg https://www.ttrinity.jp/product/5734710 2020-01-01 00:00:00.000000 2020-01-01 00:00:00.000000
2 アデリーペンギンとスーパーカブ http://okachanblog.com/static/20200315tshirt_list/tshirt_2.jpg https://www.ttrinity.jp/product/2524167 2020-01-01 00:00:00.000000 2020-01-01 00:00:00.000000
3 トリケラトプス http://okachanblog.com/static/20200315tshirt_list/tshirt_3.jpg https://www.ttrinity.jp/product/2864627 2020-01-01 00:00:00.000000 2020-01-01 00:00:00.000000
4 ワニ浮き輪 http://okachanblog.com/static/20200315tshirt_list/tshirt_4.jpg https://www.ttrinity.jp/product/3206455 2020-01-01 00:00:00.000000 2020-01-01 00:00:00.000000
5 ツッパリライオン http://okachanblog.com/static/20200315tshirt_list/tshirt_5.jpg https://www.ttrinity.jp/product/5776816 2020-01-01 00:00:00.000000 2020-01-01 00:00:00.000000

やりやすい、方法で流し込みしてもらって構いません。
私は、このくらいの量だと、手動で流しこんじゃいます。

3a03835fa8541d90bdef4ef4552d7f7e.gif

こんな感じです。(モタモタ...)

[6]テーブルに入れた情報が全部表示されるか見て見ましょう

まずは、controllerで、tshirtsテーブルから情報を全部持って来ちゃいましょう。
下記のようにファイルを修正してください。

app/controllers/tshits_controller.rb
class TshitsController < ApplicationController
  def index
    @tshirts = Tshirt.all
  end
end

とりあえず、タイトルだけ繰り返し表示させてみる。

app/views/tshrts/index.html.haml
- @tshirts.each do |t|
  = t.title

-# ~~~~~~~省略~~~~~~~

上記のようにファイルを修正して、ブラウザで見てみましょう。
邪魔であれば、先ほどペーストしたパネルをコメントアウトしちゃいましょう。

eb89eb7882d77c647d3b6b390fa9899d.png

こんな感じに表示されましたか?

[7]部分テンプレートを作っちゃえ

app/views/layouts/_tshirt_list.html.hamlを作成しましょう。
そして、下記のように編集してください。

app/views/layouts/_tshirt_list.html.haml
%li
  = link_to "https://www.ttrinity.jp/product/5734710", target: :_blank do
    = image_tag "http://okachanblog.com/static/20200315tshirt_list/tshirt_1.jpg", width: '100%'
    %br/
    %span Tシャツの名前

部分テンプレートをpartialで表示させましょう。
さて、大詰めですね。
下記のようにファイルを編集しましょう。

app/views/tshrts/index.html.haml
%ul
  = render partial: 'layouts/tshirt_list', collection: @tshirts, as: 'tshirt'

一旦、ここまで。
1f2823385e322ed2cc5a8d1a8d6e1e14.png

このように、同じ画像が繰り返されましたか?
次に、テーブルの各カラムを反映させていきます。

[8]テーブルの情報がパネルに反映されるようにしちゃえ

app/views/layouts/_tshirt_list.html.haml
%li
  = link_to tshirt.url, target: :_blank do
    = image_tag tshirt.image, width: '100%'
    %br/
    %span
      = tshirt.title

上記のように、各所にメソッドを書き込んでいきましょう。
なぜtshirt.hogeというメソッドの書き方になのか?
それでは、partialの記述をもう一度みてみましょう。

4676e9b805cf42003b474ebca4d8c983.png

上記のようになっているので、@tshirtsを中身をtshirtで取り出し、部分テンプレート内で使用しています。

d6941f061beebf513046f5abd5ff8a48.gif

どうですか?実装できましたでしょうか。

おわりに

partialは覚えてしまえばどうってことないですが、
最初はかなり意味不明でした。
説明を読んでもよくわからないタチなので、サクッと実装して勉強できればもっと早くわかったのにな〜と思ったので書いてみました。
省略して書く方法もありますが、ここでは解説しませんので、下記の記事をみてみてください!

railsで部分テンプレートについてまとめてみた

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

非同期通信

非同期通信(Ajax)

画面をリロードしなくても情報が反映されるWebアプリケーションの機能です。例えばTwitterだと更新しなくても情報がされますね。
非同期通信はAjax(エイジャックス)と呼ばれます。
Ajaxでは、レスポンスのデータにJSONという形式が使われることが多いです。

JSON

データ交換を行うためのデータ記述形式の一種です。Rubyのハッシュと同様、キーとバリューの組み合わせでデータを表現する形式です。
【例】

xxxx.js
{user_name: "testさん", created_at: "2020-03-17T10:35:13.000+09:00", content: "これがJSONの形です", image_url: null, id: 5}

非同期通信ではJavaScriptを利用

同期通信では、フォームのinputタイプがsubmitであるボタンを押すことでリクエストを行うことができました。ボタンを押すとリクエストが送られるという挙動は、HTMLであらかじめ定められているものです。

デフォルトアクション

HTMLの要素を操作した際に定められている挙動です。aタグのようにクリックされると、リンク先のページを開く、という挙動です。
対して、今回の非同期通信ではJavaScriptのメソッドを利用してリクエストを送ります。そのため、フォーム要素のデフォルトアクションを無効にする必要があります。
リクエストに対してのレスポンスはJSON形式で返してほしい旨をリクエストに含めます。

コントローラでJSON形式のデータを用意する

同期通信の際は特に指定せずともHTML形式のデータを返すようRailsが動いてくれます。
非同期通信をする際は、リクエストにJSON形式で返してほしい旨の情報が含まれているため、その場合の対処をコントローラのアクションに明記する必要があります。

レスポンスするためのJSON形式のデータを準備

同期通信ではviewsディレクトリの中に○○.html.erbという形式でHTMLのファイルを準備して置くことでレスポンスとしてHTMLを返します。
非同期通信の場合、JSONのデータをレスポンスとして返す必要があります。同期通信の際と同様viewsフォルダの中にJSON形式のファイルを作成します。この時のファイル名は、○○.json.jbuilderという形式になります。

JavaScriptでレスポンスを受け取り、HTMLを操作してToDoを追加

非同期通信では、ページがリロードされることはありません。代わりに、レスポンスとして帰ってきたJSONのデータを利用してHTMLを操作します。
JSONのデータはユーザーが投稿したToDoのデータなので、これをToDoリストの一番後ろに付け加えるようJavaScriptを書きます。

respond_to

「リクエストがHTMLのレスポンスを求めているのか、それともJSONのレスポンスを求めているのか」を条件に条件分岐してくれます。
【例】

xxxx.rb
respond_to do |format|
  format.html { render ... } # この中はHTMLリクエストの場合に呼ばれる
  format.json { render ... } # この中はJSONリクエストの場合に呼ばれる
end

HTMLを返す場合は、該当するビューを呼びその中でデータを生成しますが、JSONを返す場合はRubyのハッシュをrenderメソッドの引数として渡すだけでJSONに変換されます。そのため、以下のようにコントローラーから直接データを返すことができます。
【例】renderというメソッドに{json: { id: @user.id, name: @user.name }}というハッシュを引数として渡しています。

xxxx.rb
respond_to do |format|
  format.json { 
    render json: { id: @user.id, name: @user.name }
  }
end

JSONでレスポンスできるように

【例】

todos_controller.rb
def create
  @todo = Todo.create(todo_params)
  respond_to do |format|
    format.html { redirect_to :root }
    format.json { render json: @todo}
  end
end

respond_to doを使用し、リクエストされたformatによって処理を分けるようにします。今回はhtmlと非同期通信のためのjsonを扱うようにしました。フォーマットがjsonの時、この後jsファイル側で作成したtodo(@todo)を使用するためにrenderメソッドを使用し、作成したtodoをjson形式で返すようにします。

JavaScriptを記載して送信時に要素を取得

今回は、todo.jsというファイルをassets/javascripts以下に作成します。
Ruby on Railsでは、アプリケーション作成時にjquery-railsというgemをインストールし、assetsディレクトリ以下のapplicaton.jsファイルで//= require jqueryこのように記述することでjQueryを読み込んでいます。
【例】todo.jsにTodoの一覧ページからフォームが送信された時に、フォームに入力された値をコンソールに出力します。

todo.js
$(function() {
  $('.js-form').on('submit', function(e) {
    e.preventDefault();
    var todo = $('.js-form__text-field').val();
    console.log(todo);
  });
});

submitイベントを使い、フォーム(js-form)が送信された時に処理が実行されるようにイベントを設定します。
フォームが送信された時に、デフォルトだとフォームを送信するための通信がされてしまうので、preventDefault()を使用してデフォルトのイベントを止めます。

非同期通信でリクエスト

処理の流れ
1.フォームの送信が行われた時にAjaxによる非同期通信を始める
フォームに入力された値を取得する
Ajaxを行う記述をする
2.TodosコントローラのcreateアクションにてTodoの保存を行う
3.処理後にjsonを返す
4.非同期通信の終了後に受け取ったjsonを利用してHTMLを構築する
5.4で構築したHTMLをViewに差し込む
【例】フォームの送信が行われた時にAjaxによる非同期通信を始めるフォームの送信が行われた時にAjaxによる非同期通信を始める

todo.js
$(function() {
  $('.js-form').on('submit', function(e) {
    e.preventDefault();
    var todo = $('.js-form__text-field').val();
    $.ajax({   #追記〜
      type: 'POST',
      url: '/todos.json',
      data: {
        todo: {
          content: todo
        }
      },
      dataType: 'json'
    })   #〜追記
  });
});

$.ajax

jQueryで非同期通信を行うための記述です。.ajaxの部分がメソッドの呼び出しとなっています。
ajaxメソッドには、Rubyのハッシュのようなキーバリュー形式で引数を渡します。JavaScriptでは、このような形式のオブジェクトはオブジェクト型と呼ばれます。読みやすいように改行して縦に並べていますが、以下のようなキーとバリューの組み合わせです。
{type: 'POST', url: '/todos.json', data: { todo: { content: todo } }, dataType: 'json' }
dataというキーに対してのバリューはオブジェクト型であり、さらにそのバリューもオブジェクト型になるという、入れ子構造となっています。
Ajaxで非同期通信に必要なオプションを設定しており、それぞれの意味は以下のようになります。
type、、、HTTP通信の種類を記述する。通信方法は、GETとPOSTの2種類がある。
url、、、リクエストを送信する先のURLを記述する。
data、、、サーバに送信する値を記述する。
dataType、、、サーバから返されるデータの型を指定する。
通信方法はPOSTで、'/todos.json'というURLに{ todo: { content: todo(テキストフィールドに入力された値)} }を送信する。サーバから値を返す際は、jsonになります。

todo.js
$(function() {
  $('.js-form').on('submit', function(e) {
    e.preventDefault();
    var todo = $('.js-form__text-field').val();
    $.ajax({
      type: 'POST',
      url: '/todos.json',
      data: {
        todo: {
          content: todo
        }
      },
      dataType: 'json'
    })
    .done(function(data) {   #追記〜
      var html = $('<li class="todo">').append(data.content);
      $('.todos').append(html);
      $('.js-form__text-field').val('');
    })
    .fail(function() {
      alert('error');
    });   #〜追記
  });
});

doneとfail

ajaxメソッドの後につけることで、非同期通信が成功した際/失敗した際に行う処理を書くことができます。両方とも、ajaxメソッドとセットとなるメソッドです。doneは通信が成功したときに、failは通信が失敗したときに動きます。

data

doneのうしろにあるdataという変数には、リクエストによって返ってきたレスポンスが代入されます。この場合のレスポンスは、非同期通信によって作成したTodoにあたります。

todos_controller.rb
def create
  @todo = Todo.create(todo_params)
  respond_to do |format|
    format.html { redirect_to :root }
    format.json { render json: @todo}  #追記
  end
end

doneにもfailにも、メソッドの引数としてやってほしい処理を記述します。
今回はdoneのあとに、取得したJSONからli要素を作成しtodo一覧のリストに追加し、フォームに入力された値を空にする処理を記述しています。failのあとにはエラーが起きたことを示すアラートを表示する処理を記述しています。
最後に、htmlを生成するvar html = $('

').append(todo.content);という処理をメソッドにして切り出します。メソッドにして処理を分けることで、長くなりがちな処理を整理し、理解しやすくします。
todo.js
$(function() {
  function buildHTML(todo) {   #追記〜
    var html = $('<li class="todo">').append(todo.content);
    return html;   
  }   #〜追記

  $('.js-form').on('submit', function(e) {
    e.preventDefault();
    var todo = $('.js-form__text-field').val();
    $.ajax({
      type: 'POST',
      url: '/todos.json',
      data: {
        todo: {
          content: todo
        }
      },
      dataType: 'json'
    })
    .done(function(data) {
      var html = buildHTML(data);   #追記
      $('.todos').append(html);
      $('.js-form__text-field').val('');
    })
    .fail(function() {
      alert('error');
    });
  });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Heroku]デプロイ失敗から得られた2つの知見[備忘録]

tl;dr

1. herokuではmigration fileは日付順にmigrateされるため、外部キーを設定したmigration fileがある場合は、migrateの順番を考慮する。

2. heroku logsより詳細な情報が欲しいならheroku run rails cを使う


環境

ruby 2.6.5
rails 5.1.6.2
heroku/7.39.0 linux-x64 node-v12.13.0


問題1

heroku run rails db:migrateを行うと以下のようなエラーが発生した

PG::UndefinedTable: ERROR:  relation "test_types" does not exist
: CREATE TABLE "param_values" ("id" bigserial primary key, "name" character varying, "test_type_id" bigint, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL, CONSTRAINT "fk_rails_j74mou9ds5"
FOREIGN KEY ("test_type_id")
  REFERENCES "test_types" ("id")
)

原因

親(参照元)のテーブルが作成される前に子のテーブルが作成されているから

対処

手動でmigration fileの名前を変更することで
migrationの順序を変更した。

//[timestamp]ではなく番号を割り振った

[timestamp]_create_test_types.rb
       ↓
01_create_test_types.rb

問題2

Herokuへのデプロイ後にサイトを開くとApplication errorのエラー画面が表示された

原因

heroku logsをしてみると
at=error code=H10 desc="App crashed"と出力された。
さらに、heroku run rails cをすると、
新規に追加したテーブルのカラム名のタイポが原因であることが分かった。

対処

タイポを修正し、再びgit push herokuする

参考文献

[PG::UndefinedTable: ERROR: relation "XXXXXX" does not exist] への対応

https://qiita.com/after4649/items/0144ee436d82822d2535

[Ruby on Rails Tutorial]Herokuにデプロイ後Application error[H10 (App crashed)]が発生した時の対処法

https://qiita.com/m-itoidcf/items/77d064147a32169b5449

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

[Rails]hamlでのform_with/form_for/form_tagの書き方

はじめに

hamlでのformの書き方がまとまっていなかったのでまとめる

form_tagform_forRails5.1で非推奨となっており、将来的にform_withに置き換えられる予定です。
極力、Rails5.1以上form_withの使用を推奨します。

Railsのformの種類

1. form_tag

  • 関連するモデルがない時に使用する(検索機能など)
  • inputタグを用いる(ビルドヘルパーを用いない)
<%= form_tag users_path do %>
  <%= text_field_tag :email %>
  <%= submit_tag %>
<% end %>

2. form_for

  • 関連するモデルがある時に使用する(投稿機能など)
  • ビルドヘルパー(form.xxxx)を用いる
<%= form_for @user do |form| %>
  <%= form.text_field :email %>
  <%= form.submit %>
<% end %>

3. form_with

  • Rails5.1以上は問答無用で ###form_tag/form_forとの違い
  • form_withで自動でパスが選択されるので、HTTPメソッド(getpostなど)を指定する必要が無い
  • コントローラーから渡されたActiveRecordを継承するモデルのインスタンスが利用できる
  • form_withは、form_forとform_tagの機能を組み合わせたもの、とイメージすると分かりやすい
#関連するモデルがない場合 → urlの指定のみで、modelの記述がない
<%= form_with url: users_path do |form| %>
  <%= form.text_field :email %>
  <%= form.submit %>
<% end %>
#関連するモデルがある場合 → modelの記述のみで、urlの指定は不要
<%= form_with model: @user do |form| %>
  <%= form.text_field :email %>
  <%= form.submit %>
<% end %>

hamlでの書き方

この記事のメインはこちら

form_tag

ビルドヘルパー(f.text_fieldなど)は記述しない点に注意

= form_tag users_path do 
  = text_field_tag :email
  = submit_tag

form_for

= form_for @user do |f|
  = f.text_field :email
  = f.submit

form_with

#関連するモデルがない場合 → urlの指定のみで、modelの記述がない
= form_with url: users_path do |f|
  = f.text_field :email
  = form.submit
#関連するモデルがある場合 → modelの記述のみで、urlの指定は不要
= form_with model: @user do |f|
  = form.text_field :email
  = form.submit

参考記事

【Rails】form_for/form_tagの違い・使い分けをまとめた
【Rails 5】(新) form_with と (旧) form_tag, form_for の違い

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