- 投稿日:2020-03-18T22:54:40+09:00
ローカルで行った変更点が本番環境に反映されない時の対処法
せっかくローカルで変更して上手くいっても、本番環境で変化がなければなんの意味もありません。
今回、ローカルホストではしっかり変わっているにも関わらず、本番環境(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を再起動してくれます。
私のような誰かの助けになれば幸いです!!!
それではまたどこかで^^
- 投稿日:2020-03-18T22:09:46+09:00
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接続できずにエラー発生。
ググる。
ポート番号がすでにローカルで使われているかもしれない!という記事にたどり着く。
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の方が扱いやすいかもですね。
- 投稿日:2020-03-18T20:09:42+09:00
railsで動画サイトにいいね機能実装
動画サイトにいいね機能を実装してみた
今では当たり前になりつつあるいいね機能ですが、どのように作られているか気になりポートフォリオに試しに実装してみました。1ユーザーが複数の動画に対していいねをし、また1動画に対して複数のユーザーがいいねする、いわば多対多の関係なので、ユーザーと動画の中間テーブルとしていいねテーブルを作成していきます。
完成図
環境
rails : v5.2.4.1
〜実装済み機能〜
・動画(一覧表示、新規投稿、詳細、編集、削除)
・ユーザー(新規登録、ログイン、編集、ログアウト、詳細)
・ページネーション実装手順
※いいね機能に関する部分のみコメントアウトで説明書きしています。
- ターミナルからLikeモデル作成
$ rails g model Like user:references video:references $ rails db:migratemodels/user.rbclass 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 endmodels/video.rbclass 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 endmodels/like.rbclass 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.rbRails.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 endcontrollers/likes_controller.rbclass 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 endcontrollers/videos_controller.rbclass 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.countvideoの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に書き換え
参考
- 投稿日:2020-03-18T18:49:33+09:00
【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/'` 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で無事にデータベースが作成できました。
- 投稿日:2020-03-18T15:28:24+09:00
【Rails】has_secure_password
has_secure_password
user.rbclass 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
の機能は動作しない。
- 投稿日:2020-03-18T14:41:48+09:00
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をし直すと、、、
できた!!!!!
- 投稿日:2020-03-18T13:07:33+09:00
【Rails】URLテキストにaタグを自動でつける
はじめに
ユーザーがフォームに投稿したURLテキストに自動でaタグをつけて表示できたらなーーと調べていたら、uriライブラリーのおかげでとても簡単にできたので、備忘録としてまとめます。
uriライブラリーを読み込む
Rubyの標準ライブラリーとして'uri'というURIを扱う標準ライブラリ-があるので、読み込みます。
https://docs.ruby-lang.org/ja/latest/class/URI.htmlapp/helpers/application_helper.rbrequire "uri"ヘルパーメソッドを作成
uriライブラリーを読み込んだapplication_helper.rbでメソッドを作成します。
app/helpers/application_helper.rbrequire "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
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タグをつけて表示してくれるようになります。
- 投稿日:2020-03-18T12:08:04+09:00
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.rbrequire '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.rbrequire '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.rbrequire '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分でおさらいしよう!
- 投稿日:2020-03-18T12:08:04+09:00
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.rbrequire '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.rbrequire '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.rbrequire '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.rbrequire '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分でおさらいしよう!
- 投稿日:2020-03-18T11:57:13+09:00
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.ymlversion: '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:4444gemを追加する
Gemfilegroup :development, :test do gem 'rspec-rails' end group :test do gem 'capybara', '>= 2.15' gem 'selenium-webdriver' enddockerをbuildして、bundle installします。
$ docker-compose build $ docker-compose up -drspecの設定
rspecをインストールします。
$ docker-compose run web rails g rspec:installheadless chromeの設定
spec/rails_helper.rbにheadless chromeの設定を追加していきます。
/spec/rails_helper.rbrequire '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 =30003001
とすることで、競合することなく、無事サーバーが立ち上がり、テストが通るようになりました。これで準備は整いました。
あとは、テストを実際に走らせるだけです。実際にテストを走らせてみる
spec/system/test_spec.rbrequire "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をやってみた。
- 投稿日:2020-03-18T09:26:19+09:00
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.rbdef self.micropost_serach(search) Micropost.where(['title LIKE ?', "%#{search}%"]) endwhereメゾットとLIKE旬でマイクロポストの中からあいまい検索をします。
モデル名.where([カラム名前 LIKE ?, "検索したい文字列"])検索したい文字列の両サイドにある「%」は任意の複数の文字列を表しています。
上記のは一つしかカラムを指定していませんが複数指定することもできます。app/model/micropost.rbdef self.micropost_serach(search) Micropost.where(['title LIKE ? OR content LIKE ?', "%#{search}%", "%#{search}%"]) end複数のカラムを指定する場合はカラム数に応じてORやANDで繋ぎ、検索したい文字列を増やしてあげると検索できます。
今回はさらに、マイクロポストにコメントされた文章も含めて検索してみます。
コメント機能についてこちら
https://qiita.com/E6YOteYPzmFGfOD/items/ef776d34908872ea19f7app/model/micropost.rbdef 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これでコメント内も合わせて検索できるようになりました
#終わりに
最後までお読みいただきありがとうございました。検索文はいろいろパターンがあるので今後も新しい物を書いたときには記事にしてみようと思います。
ありがとうございました。
- 投稿日:2020-03-18T07:07:41+09:00
ツイッター風Railsアプリ最短復習(忙しい人の流し読みで開発シリーズ)
はじめに
こんにch… え?忙しい?? んじゃぁスタート!!!
具体的な手順
①アプリ立ち上げ
Terminal$ cd Desktop $ rails _5.2.4.1_ new cheaptweet -d mysql $ cd cheaptweet $ rails db:create $ rails swebBrowserlocalhost:3000②テーブル作成
Gemfile# 省略 gem 'devise'Terminal$ bundle install $ rails g devise:install control + c $ rails s $ rails g devise userdb/migrate/2020xxxxxxxxx_devise_create_users.rb# 省略 t.string :nickname, null: false # 省略Terminal$ rails db:migrateTerminal$ 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
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.rbclass 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 endTerminal$ 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>④投稿(new→create)
config/routes.rb# resources :tweets, only: [:index] # | # V resources :tweets, only: [:index, :new, :create] #省略app/controllers/tweets_controller.rbclass 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 ####↑追記↑################################################# endnew.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> <!--省略-->⑤一覧(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 %>⑥詳細(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.rbclass 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 installTerminalcontrol + 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) %>⑨コメント機能
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
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 endapp/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.rbclass 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 endapp/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 %>⑩ユーザー投稿一覧
Terminal$ rails g controller users showconfig/routes.rb# devise_for :usersより下に #省略 resources :users, only: :show #省略app/controllers/users_controller.rbclass 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 endapp/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) %>⑪検索機能
Terminalrails g controller tweets::searchesconfig/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.rbclass Tweets::SearchesController < ApplicationController def index @tweets = Tweet.search(params[:keyword]).order("created_at DESC").page(params[:page]).per(5) end endapp/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苦手の自分とお別れしよう)
- 投稿日:2020-03-18T07:03:20+09:00
「Rails」「初心者向け」ダイレクトメッセージ機能を1行ずつ解説
初めに
この記事は、Railsでダイレクトメッセージ機能を実装する方法について紹介します。
私自身が実装するにあたり、参考にさせて頂いた記事があります。
したがって、この記事の大部分は、それらの記事の内容に依っていますが、一部、私自身のしたいことがあり、応用した部分もあります。この記事は、Railsチュートリアルを終えたレベルの初心者向けの記事です。
したがって、中級者以上の方が読まれると、冗長な文章に感じられると思います。ただ、初心者にとっては、当然の前提とされていることが、解説されていないことで詰まることは多いので、なるべく詳しく書こうと考えています。参考文献
https://qiita.com/nojinoji/items/2b3f8309a31cc6d88d03
https://iberiko665.hatenablog.com/entry/2019/03/03/215730ダイレクトメッセージとは...
そもそも、私はダイレクトメッセージが、投稿やチャットとどのように異なるのか理解していなかったので、一応整理しておきます。私としては以下のように分類しています。
- 投稿
誰でも参加できる。誰でも投稿された文章、画像を見ることが出来る。- ダイレクトメッセージ
基本的に一対一や少数で他から見えない形で部屋が作られ、コメント出来る。- チャット
通常のHTTP通信と違い、web socketを使用している。 大きな違いとして、ユーザーがリロードせずに保存されたデータが表示される仕組みにできる(リアルタイム性がある)。実装例
商品詳細画面(products/show)から「チャットを始める」ボタンを押すと、新たにルーム(rooms/show)が作られます。そのルームで、一対一でコミュニケーションを取ることが出来ます(「チャット」としていますが、上記の定義から言えば、「ダイレクトメッセージ」となります)。
app/views/products/show.html.erb
注意
この画像はあくまでイメージです。
添付の画像は、私が制作したアプリの一部をスクリーンショットしたものです。
今回、添付の画像のようなデザイン(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.rbhas_many :products has_many :messages, dependent: :destroy has_many :memberships, dependent: :destroyproduct.rbbelongs_to :user has_one :roomroom.rbhas_many :messages, dependent: :destroy has_many :memberships, dependent: :destroy belongs_to :productmembership.rbbelongs_to :user belongs_to :roommessage.rbbelongs_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.rbdef 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 end1行ずつ解説していきます。
@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
@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
では、url
とlocal
を指定する必要があります。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(パラメータ)は以下の通りです。
全体像の把握の章でも説明したように、このチャットを始めるボタンを押した際、送信先は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_field
とhidden_field_tag
であった部分を変更している点が異なります。このように変えた場合、どのように値が渡るのかというと、以下のように渡ります。
注目していただきたいのは、
"product"=>{ "product_id"=>"1", user_id"=>"1"}
となっている部分です。
このようにhidden_field
を使用すると、model
として指定した値の内部にhidden_fieldで指定した内容が格納されます。そのため、f.hidden_field
とform_with
のフォームビルダーをレシーバとして記述する必要があります。
つまり、(product)モデルに紐づく形式で値が渡っているということです。-補足-
フォームビルダーとは、form_withなどdo-endで囲まれた部分をブロックと呼びますが、そのブロック内部で|f|と設定しているもののことです。
レシーバとは、User.find
のような記述でのUserに当たるモノです。findというメソッドを受ける対象と考えると分かりやすいです。結論として、今回のproducts/show.html.erbからrooms#createへはモデルに紐づく形で値を渡す必要がなかったので、
hidden_field_tag
を使用しています。さらに、これは余談ですが、
hidden_field
もhidden_field_tag
も第一引数は「どのように値を受け取りたいか」、第二引数は「渡す値」を指定します。
私は、これまでform_with
のf.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.rbdef 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 endrooms#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カラムに代入しているということになります。コードの出力から見ると、
@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.rbdef 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 endrooms#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.rbclass 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 endmessages#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_with
でhidden_field
を使用し、hidden_field_tag
を使用しなかった理由から説明します。
それはMessageモデルに紐づく形で値を渡したかったからです。
つまり、値は
このように、"message"=>{ "content"=>"Goodby", ... }といった形式で渡したい。
なぜなら、ストロングパラメータとして更新するにはそのように形式が整っていないと更新できないからです。
ここまでは、前回のhidden_field
とhidden_field_tag
の違いをよく考えてみれば、分かると思います。さらに、Messageモデルはcontent、user_id、room_idの三つのカラムのデータを更新する必要があります。
contentはメッセージの内容、user_idはどのユーザーが投稿したメッセージか、room_idはどのルームで投稿されたのか、という情報でMessageモデルには全て必須です。しかし、スクショを見ると、"message"=>以下のハッシュにcontent, room_idはありますが、user_idがありません。したがって、このままでは、値が更新できず、
このスクショのように、ROLLBACK、つまり何か不具合があったので、保存しませんというサインが出ています。したがって、「user_idも同時に更新したいので、この値を入れてください」とお願いするために、
merge
メソッドを使用します。
merge
メソッドの公式的な説明は「ハッシュの結合」です。
つまり、ストロングパラメータとしては、"message"=> { ... }というMessageモデルのハッシュに、user_idを強引にカットインさせるために使用します。
具体的には、
.merge(user_id: current_user.id)
とすることで、そのハッシュに入れることが出来ます。
それはbinding.pry
でmerge
メソッドを記述した際、記述しなかった際を比較してみると分かると思います。このように、
merge
メソッドを使用すると、パラメータの内部にうまく入れることが出来ました。あとは、リダイレクトだけなので、説明は省きます。
products#show - 一度ルームを作成している場合
これから、すでにルームを作成し、再び同じルームに入る場合の処理について解説していきます。
もう一度該当のコードを載せます。products_controller.rbdef 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 endrooms#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行ずつ意味を考えてみて、その精緻さに感動しました。私は、ある商品に紐づく形でダイレクトメッセージ機能を実装したいと思っていました。
人それぞれ、実装したい形は異なっていると思うので、この記事がそうした応用に役立てばいいと思い、共有します。
- 投稿日:2020-03-18T07:02:21+09:00
RspecでProcess.fork内のメソッドを検証したい
fork内のメソッドコールを単純に検証するとうまくいかない
こんなクラスがあるときに
class Dog def crow(str) p str end def walk Process.fork do crow('bowbow!') end end endwalkメソッド内で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 timesforkブロック内は別プロセスなので検証できないのですね。
結論
こういう場合は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!")ちゃんと検証してくれているようです。
参考
- 投稿日:2020-03-18T03:20:38+09:00
[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行ずつ挟んで確認すれば原因を特定しやすくなり、エラーを見つけることができます。
- 投稿日:2020-03-18T02:29:07+09:00
【rails】partialをサクっと実装してちょっと覚える
partialは部分テンプレートを使用して、同じパターンの塊を複数生成するのに便利なものです。
eachでもできますが、処理速度がpartialの方が早いらしいですよ。そんなpartialですが、理解するのに苦戦した記憶があります。
partialを知らなかった頃の自分に読ませたい記事をかけたらなと思います。使うもの
・Ruby on Rails
・mysql
・haml
$ rails s
でハローワールドを表示できる環境を整えてください。作れるもの
[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.scssbody,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; }どうでしょうか、ここまでは表示できましたか?
[4]modelを作っちゃいましょ
ターミナルで、
$ rails g model tshirt
とマイグレート ファイルを生成しましょう。20200XXXXXXXX_create_tshirts.rbclass 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 やりやすい、方法で流し込みしてもらって構いません。
私は、このくらいの量だと、手動で流しこんじゃいます。こんな感じです。(モタモタ...)
[6]テーブルに入れた情報が全部表示されるか見て見ましょう
まずは、controllerで、tshirtsテーブルから情報を全部持って来ちゃいましょう。
下記のようにファイルを修正してください。app/controllers/tshits_controller.rbclass TshitsController < ApplicationController def index @tshirts = Tshirt.all end endとりあえず、タイトルだけ繰り返し表示させてみる。
app/views/tshrts/index.html.haml- @tshirts.each do |t| = t.title -# ~~~~~~~省略~~~~~~~上記のようにファイルを修正して、ブラウザで見てみましょう。
邪魔であれば、先ほどペーストしたパネルをコメントアウトしちゃいましょう。こんな感じに表示されましたか?
[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'このように、同じ画像が繰り返されましたか?
次に、テーブルの各カラムを反映させていきます。[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の記述をもう一度みてみましょう。上記のようになっているので、
@tshirts
を中身をtshirt
で取り出し、部分テンプレート内で使用しています。どうですか?実装できましたでしょうか。
おわりに
partialは覚えてしまえばどうってことないですが、
最初はかなり意味不明でした。
説明を読んでもよくわからないタチなので、サクッと実装して勉強できればもっと早くわかったのにな〜と思ったので書いてみました。
省略して書く方法もありますが、ここでは解説しませんので、下記の記事をみてみてください!
- 投稿日:2020-03-18T01:57:42+09:00
非同期通信
非同期通信(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.rbrespond_to do |format| format.html { render ... } # この中はHTMLリクエストの場合に呼ばれる format.json { render ... } # この中はJSONリクエストの場合に呼ばれる endHTMLを返す場合は、該当するビューを呼びその中でデータを生成しますが、JSONを返す場合はRubyのハッシュをrenderメソッドの引数として渡すだけでJSONに変換されます。そのため、以下のようにコントローラーから直接データを返すことができます。
【例】renderというメソッドに{json: { id: @user.id, name: @user.name }}というハッシュを引数として渡しています。xxxx.rbrespond_to do |format| format.json { render json: { id: @user.id, name: @user.name } } endJSONでレスポンスできるように
【例】
todos_controller.rbdef create @todo = Todo.create(todo_params) respond_to do |format| format.html { redirect_to :root } format.json { render json: @todo} end endrespond_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.rbdef create @todo = Todo.create(todo_params) respond_to do |format| format.html { redirect_to :root } format.json { render json: @todo} #追記 end enddoneにもfailにも、メソッドの引数としてやってほしい処理を記述します。
').append(todo.content);という処理をメソッドにして切り出します。メソッドにして処理を分けることで、長くなりがちな処理を整理し、理解しやすくします。
今回はdoneのあとに、取得したJSONからli要素を作成しtodo一覧のリストに追加し、フォームに入力された値を空にする処理を記述しています。failのあとにはエラーが起きたことを示すアラートを表示する処理を記述しています。
最後に、htmlを生成するvar html = $('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'); }); }); });
- 投稿日:2020-03-18T01:36:30+09:00
[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] への対応
[Ruby on Rails Tutorial]Herokuにデプロイ後Application error[H10 (App crashed)]が発生した時の対処法
- 投稿日:2020-03-18T00:09:20+09:00
[Rails]hamlでのform_with/form_for/form_tagの書き方
はじめに
hamlでのformの書き方がまとまっていなかったのでまとめる
※
form_tag
とform_for
はRails5.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メソッド(
get
やpost
など)を指定する必要が無い- コントローラーから渡された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_tagform_for
= form_for @user do |f| = f.text_field :email = f.submitform_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 の違い