- 投稿日:2021-03-21T23:19:34+09:00
Active Recordのpluckとselect
- 投稿日:2021-03-21T22:46:59+09:00
ActiveRecord preload, eager_load, include, joinについて
ActiveRecord preload, eager_load, include, joinについて、以下の記事が一番分かりやすい。
- 投稿日:2021-03-21T22:34:50+09:00
Railsのコントローラー上で.envを使った環境変数を読み込めなかった時の対応
実現したいこと:
Railsのコントローラー上で、
.envのファイルに設定した環境変数を読み込んでAPIを取得したい。環境:
Ruby 2.6.6
Rails 6.0.3.4
gemのdotenv-railsをインストール済状況:
Ruby-on-RailsでSNSのアプリケーションを作成中にNewsApiを使用し、
Newsの情報を取得し表示する機能を実装していた。
APIKEYを.envファイルにて環境変数に置き換えて使用したかったが、
エラーが発生して読み込めなかった。ニュース機能の実装で参考にした記事:
https://qiita.com/UTOG/items/f6438420e81b6488a508
上記執筆者様の実装方法を参考にNewsApiへ登録し、
APIKEYを取得し環境変数として設定後、記載されている実装コードを活用しファイル作成。(ニュースカテゴリー等の設定は変更)対象ファイル:
app/controller/news_Controller.rbclass NewsController < ApplicationController def index end def data uri = URI.parse('http://newsapi.org/v2/top-headlines?country=jp&category=business&pageSize=15&apiKey=<%="#{ENV['API_KEY']}"%>') json = Net::HTTP.get(uri) moments = JSON.parse(json) @data = moments['articles'].to_json end end.envAPI_KEY='この中に取得したAPI_KEYを入力'上記コードでは下記のようなエラーが発生。
エラー文:
syntax error, unexpected tCONSTANT, expecting ')'
...ze=15&apiKey=<%="#{ENV['API_KEY']}"%>')
... ^~~~~~~
/app/controllers/news_controller.rb:6: syntax error, unexpected ')', expecting end
...iKey=<%="#{ENV['API_KEY']}"%>')
調べたところ閉じタグがないと警告が出ており、
どうやら環境変数が読み込めていないため、
そこでコードの読み込みが止まってしまっているようあった。試したこと:
1.環境変数を挿入していた該当のURLに取得したAPIKEYをベタ打ちで挿入
→ニュースが問題なく表示される。2.コンソール上で環境変数を実行( 実行コード:
$ ENV['API_KEY']
)
→.env内に記載していたAPIKEYが問題なく表示される。→この時点で、
①ニュースを表示すること
②.env内での環境変数の設定はできていると予想。
コントローラーへの環境変数の組み込み方に問題があるのではと仮定。2.環境変数の挿入方法を変更
(news.controller.rb 6行目を下記のパターンで変更 ※対象箇所周辺コードのみ抜粋)country=jp&category=business&pageSize=15&apiKey=<%="#{ENV['API_KEY']}"%>') country=jp&category=business&pageSize=15&apiKey=<%=ENV['API_KEY']%>') country=jp&category=business&pageSize=15&apiKey=<"#{ENV['API_KEY']}"') country=jp&category=business&pageSize=15&apiKey=ENV['API_KEY']')→検索しながら上記色々なパターンを実行したものの、エラー等で読み込めない状況は変わらず。。
原因:
<% %>や<%= %>の記述はerbファイル内で使える記述であり、
erb上でRubyコードを実行したり、Rubyで定義した変数などをHTMLとして出力したりする際に利用するとのことであり、コントローラー内では記述方法は異なるとのこと。解決策:
下記、コードを修正。
ダブルクォーテーション(")とシングルクォーテーション(')の使い方にも間違いがあった模様。app/controller/news_Controller.rbclass NewsController < ApplicationController def index end def data uri = URI.parse("http://newsapi.org/v2/top-headlines?country=jp&category=business&pageSize=15&apiKey=#{ENV['API_KEY']}") json = Net::HTTP.get(uri) moments = JSON.parse(json) @data = moments['articles'].to_json end end今回、エラーにハマっていたところを質問サイトにて回答を頂いたため、
同じ状況の方のご参考になればと思い、共有させて頂きます。まだまだ未熟なため説明不足な点もございますが、
今後修正等加えて参りますので気になる点がございましたらぜひ、
ご指導、ご鞭撻のほどよろしくお願いいたします。
- 投稿日:2021-03-21T22:06:51+09:00
Tips: Rubyでresponse bodyをstreamで受け取ってNoMemoryErrorを回避する
RubyやRailsなどでHTTPリクエストを行う場合に、response bodyをstreamで受け取る方法を紹介します。
単純にリクエストをするとレスポンスの結果を全てメモリに載せてしまうので、メモリの節約や巨大なデータを取得するとNoMemoryError
が発生してしまいます。
今回の手法はこのエラーになってしまう状態を回避する方法として有効です。Net::HTTPの場合
にある通り、
read_body
というメソッドを用いることでstreamでデータの受け取りを行うことができます。ファイルに書き出すことでメモリを圧迫せずにデータを取得できます。require 'net/http' uri = URI.parse('http://example.com/') Net::HTTP.start(uri.host, uri.port) do |http| http.request_get(uri.path) do |response| File.open("file_path", "w") do |file| response.read_body do |chunk| file.write(chunk) end end end endまた、streamで取得したデータの累計サイズを記録しておくことで、特定のサイズでデータの取得を打ち切るようなこともできます。
require 'net/http' uri = URI.parse('http://example.com/') response_body = '' total_size = 0 Net::HTTP.start(uri.host, uri.port) do |http| http.request_get(uri.path) do |response| response.read_body do |chunk| response_body += chunk if total_size <= 1000000 # 1Mバイトまでのデータまで受け取る total_size += chunk.bytesize end end endFaradayの場合
にある通り、
on_data
というオプションにprocを渡すことでread_body
と同じようなことができます(このオプションが使えるのはNet::HTTP
をアダプタにしている場合のみです)require 'faraday' connection = Faraday.new(url: 'http://example.com/') response_body = '' connection.get('/') do |request| request.options.on_data = proc do |chunk, overall_received_bytes| puts "Received #{overall_received_bytes} characters" response_body += chunk end endまた、
Net::HTTP
と同じようにファイルに書き出すこともできますし、切り捨ても行えます。require 'faraday' connection = Faraday.new(url: 'http://example.com/') connection.get('/') do |request| request.options.on_data = proc do |chunk, overall_received_bytes| puts "Received #{overall_received_bytes} characters" File.open("file_path", "w") do |file| file.write(chunk) end end endrequire 'faraday' connection = Faraday.new(url: 'http://example.com/') response_body = '' connection.get('/') do |request| request.options.on_data = proc do |chunk, overall_received_bytes| puts "Received #{overall_received_bytes} characters" response_body += chunk if overall_received_bytes <= 1000000 # 1Mバイトまでのデータまで受け取る end end
- 投稿日:2021-03-21T21:45:24+09:00
Rails sして無限ループから抜けられなくなった時の解決法【超初心者】
redirect_toしたら永遠にクルクル回ってしまった!!!
コンソール$ps -ef | grep pumaすると
ec2-user 1234 23456 82 10:26 pts/3 00:04:41 puma 8.11.7 (localhost:2000)
みたいなのが出てくるコンソールkill -9 1234 #書いてある数字したらpumaがストップしました、、、
良かったです、、、
- 投稿日:2021-03-21T21:38:32+09:00
[Rails]コントローラのbefore_actionでインスタンス変数を定義するのはやめよう
前提
この記事で書いているインスタンス変数は全てview側で参照しているものとします。
まずは結論から
↓のように、view側で使うインスタンス変数をbefore_actionで初期化するのはやめましょう。
class PostsController < ApplicationController before_action :set_post, only: [:show, :edit] def show; end def edit; end private def set_post @post = Post.find params[:id] end endなぜ何故よくないのか
view側でインスタンス変数が呼び出されてるのを見たとき、普通は真っ先にコントローラーのアクションを見に行きます。
その時にアクションの中でインスタンス変数が初期化されてない場合、どこで初期化されたか探す必要が出るためコードの可読性が下がります。
↑のコードはまだ一目見てわかるようになっていますが、例えば、class PostsController < ApplicationController before_action :set_post, only: [:show, :edit] def show; end def edit; end # ここに行数が長いコード private # ここに行数が長いコード def set_post @post = Post.find params[:id] end endこのように行数の長いコードが挟まってると、画面には収まりきらずどこでインスタンス変数を初期化したか探す羽目になります。
上記のコードの場合、コントローラ名、インスタンス変数名共に
post
を使ってるので、Railsに慣れてる方なら「多分before_actionだろうな」とすぐに気が付きますが、以下のように、class PostsController < ApplicationController before_action :set_post, only: [:show, :edit] before_action :set_foo, only: [:show, :edit] before_action :set_bar, only: [:show, :edit] def show; end def edit; end # ここに行数が長いコード private # ここに行数が長いコード def set_post @post = Post.find params[:id] end def set_foo @foo = # ここに @foo 初期化処理 end def set_bar @bar = # ここに @bar 初期化処理 end endこうなってくると、Railsに慣れてる方でも探すのに苦労するようになってきます。
これならまだ
set_
のプレフィックスがあるので何とか付いていけますが、例えばcheck_role
みたいなbefore_actionで@role
インスタンス変数を初期化していると、本格的にわけわからなくなってきます。(実際にこのような書き方でインスタンス変数探すのに苦労した案件が一部あります。)解決案1
やはり、アクションで必要なインスタンス変数を初期化するのが王道でしょうか。
class PostsController < ApplicationController def show @post = get_post(params[:id]) @foo = get_foo @bar = get_bar end def edit @post = get_post(params[:id]) @foo = get_foo @bar = get_bar end # ここに行数が長いコード private # ここに行数が長いコード def get_post(id) Post.find id end def get_foo # ここに foo 取得処理 end def get_bar # ここに bar 取得処理 end end解決案2
または、以下のようにhelper_methodにしてしまってもよいと思います。
class PostsController < ApplicationController helper_method :post helper_method :foo helper_method :bar def show end def edit end # ここに行数が長いコード private # ここに行数が長いコード def post @post ||= Post.find params[:id] end def foo # ここに foo 取得処理 end def bar # ここに bar 取得処理 end endそしてview側では インスタンス変数ではなくヘルパーメソッド を呼び出します。
views/posts/show.html.erb<p><%= post.body %></p> <p><%= foo %></p> <p><%= bar %></p>これだと「before_action使ってる時とあまり変わらないじゃないか」と思う方もいるかもしれませんが、
helper_method
はviewから呼び出すことが前提のメソッドなので、インスタンス変数初期化以外の用途でも使う before_action よりは「あ、これview側で使うんだな」ということがぱっと見で伝わります。また、
helper_method
を使った場合、view側がインスタンス変数に依存しなくなるので、例えば@pest
のようなtypoをやらかしてもその場でエラーを吐くのですぐに気が付けます。まとめ
システムの規模が大きくなるとbefore_actionで初期化したインスタンス変数を探すのがしんどくなるのでやめよう。
- 投稿日:2021-03-21T21:17:41+09:00
RSpecデータリクエストを削減してテスト実行時間を削減した話
はじめに
私はエンジニアインターンで主にサーバーサイドの開発を担当させていただいています。
機能実装の際はRSpecでテストケースを書いてから実装していて、コミットする前に全体のテストを通してバグを見つけているのですが
実行時間が長い!
ということで、RSpecのリファクタリングを行いテストの実行時間を削減して見ましたので、一部紹介したいと思います。
1. 従来のRSpecでのデータリクエスト状況
現状、RSpecでは以下のように、
let
を使用してデータの作成を行っています。require 'spec_helper' describe User do let!(:user) { FactoryBot.create(:user) } it 'anything' do # userのテスト end it 'anything' do # userのテスト end it 'anything' do # userのテスト end endこれではexampleが3回行われる度に
User
モデルが作成されてしまう。
これをどうにか1回のレコード作成に留められないか...2. gem "test-prof"の導入
そこで
test-prof
を導入します。Gemfilegem "test-prof"bundle install変更後テストケース
require 'spec_helper' describe User do let_it_be(:user) { FactoryBot.create(:user) } it 'anything' do # userのテスト end it 'anything' do # userのテスト end it 'anything' do # userのテスト end endこのようにtest-profは
let_it_be
というヘルパーメソッドを提供してくれるgemになります。そうするとRailsのトランザクションテスト機能を利用してレコードを最初に1回だけ作成して、テストが終了したら該当データを削除してくれるようになります。
3. ベンチマーク
変更したテストファイル数は全体の3割ほどですが、約1分のテスト実行時間の削減に繋がりました。
let_it_be使用前
let_it_be使用後
おわりに
画期的にテストの実行時間を削減できた訳ではありませんが、長期的に見て効果のある対処法なのではないかと思います。
参考
- 投稿日:2021-03-21T21:14:20+09:00
Railsチュートリアル10章まとめ
10.1 ユーザーを更新する
ユーザーを更新する行為は、新規ユーザーの作成に似ている。
アクション リクエストに応答 新規作成 new POSTに対してcreate 更新 edit PATCHに対してupdate 10.1.1編集フォーム
まずやるべきは、Usersコントローラーにeditアクションを追加し、それに対応するeditビューを実装。
ルーティングはresources :users
のおかげで有効になっている。Railsチュートリアル 表7.1を確認すると、ユーザー編集ページのURLは
/users/1/edit
となっている。
ここで、ユーザーidを取得するために用いるのが、params[:id]
paramとは、Railsで送られてきた値を取得するメソッドapp/controllers/users_controller.rbdef edit @user = User.find(params[:id]) endユーザーのeditアクション
app/views/users/edit.html.erb<% provide(:title, "Edit user") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(model: @user, local: true) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Save changes", class: "btn btn-primary" %> <% end %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="https://gravatar.com/emails" target="_blank">change</a> </div> </div> </div>ユーザーのeditビュー
ここでは、formの部分で、大部分がユーザー作成画面とかぶっているので、パーシャルでまとめるのが良い。また、@userインスタンス変数を使って、idを取得し編集ページを表示している。
HTML<form accept-charset="UTF-8" action="/users/1" class="edit_user" id="edit_user_1" method="post"> <input name="_method" type="hidden" value="patch" /> . . . </form>WebブラウザではそのままではPATCHリクエストを送信できない。
そこで以下のコードで「偽装」している。<input name="_method" type="hidden" value="patch" />リスト 10.2のform_with(@user)のコードは、リスト 7.15のコードと完全に同じである。
ここでは、Active Record
のnew_record
論理値メソッドでPOSTリクエストとPATCHリクエストを区別している。$ rails console >> User.new.new_record? => true >> User.first.new_record? => falsetrue→POST
false→PATCH最後に、ナビゲーションのリンクを実装
<%= link_to "Settings", edit_user_path(current_user) %>
edit_user_path
もUsersリソースのおかげで使用可能。
current_userはヘルパーメソッド(9章で実装)10.1.2 編集の失敗
ゴールはupdateメソッドの実装。
app/controllers/users_controller.rbdef update @user = User.find(params[:id]) if @user.update(user_params) # 更新に成功した場合を扱う。 else render 'edit' end end抑えておきたいのは、updateの呼び出しにuser_paramsを使っている点。
これはStrong Parameters
というテクニックuser_paramsメソッドはx Usersコントローラー内部でのみ実行され、Web経由で外部ユーザーに晒される心配はなし。属性の許可を制限することで管理者権限を乗っ取られることを防ぐ。
(詳しくは7.3.2参照)private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end end10.1.3 編集失敗時のテスト
統合テストを行う
$ rails generate integration_test users_edit invoke test_unit create test/integration/users_edit_test.rbコマンドで統合テストを生成
まずは、編集失敗時のテストから。
流れとしては、編集ページにアクセス→editビューが描画されるか確認→無効な情報を送信→再度editビューが描画されるtest/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do get edit_user_path(@user) assert_template 'users/edit' patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } } assert_template 'users/edit' end endここでPATCHリクエストを送るために
patchメソッド
を使用している。
これはget
,post
メソッド等と同様に、HTTPリクエストを送るためのもの。
(HTTPリクエストとは、クライアント側からWebサーバーにリクエストする際送信するもの)
- 投稿日:2021-03-21T21:03:46+09:00
ActiveModelに対してエラーメッセージ日本語化 ja.yml書き方
投稿記事にタグ付けをするため、1つのフォーム送信で複数のモデルを更新できるというFormオブジェクトを使用しました。
app/models/posts_tag.rbclass PostsTag include ActiveModel::Model attr_accessor :title,:text,:answer,:user_id,:image, :name with_options presence: true do validates :title validates :text validates :name end def save post = Post.create(title: title,text: text,answer: answer,user_id: user_id,image: image) tag = Tag.where(name: name).first_or_initialize tag.save PostTagRelation.create(post_id: post.id, tag_id: tag.id) end endそして投稿時にエラーが発生した際に日本語でエラーメッセージが表示されるようにja.ymlを作成したのですが、英語から日本語に変換してくれませんでした。><
↓はダメだった書き方です。config/locales/ja.ymlja: activerecord: attributes: user: nickname: ニックネーム post: title: 投稿タイトル text: 投稿内容 image: 画像 name: タグ名そこから下記のように修正したところ日本語に変換してくれました!
config/locales/ja.ymlja: activerecord: attributes: user: nickname: ニックネーム activemodel: attributes: posts_tag: title: 投稿タイトル text: 投稿内容 image: 画像 name: タグ名activerecordとactivemodelの違いも分かっていないプログラミング初学者ですが、
とても良い経験となりました。おわり
- 投稿日:2021-03-21T19:57:50+09:00
RailsでとにかくDBをリセットしたい場合(メモ)
migrationファイル内を変更している時、いろいろやりすぎて一度DB関連をすべてリセットしたい時のコマンド
rails db:migrate:resetこのコマンドを実行することでデータベースを作り直すことができる。
(流れ・動き)
①データベースを削除
②データベース作成
③マイグレーション実行
この流れで実行されている。
- 投稿日:2021-03-21T18:48:09+09:00
PusherとRailsでprivate channelの通知を使ってチャットを作ってみた
概要
Railsでチャットを作るには、ActionCableが第1候補です。しかし、スケールしたときに、ActionCableを自前のサーバーで面倒見るのはちょっと心配。
ということで、外部サービスいいのないかなーと思って調べたところ、Pusherにゆきつきました。
昔からあるサービスです。しかし、公式でRubyの事例があるにもかかわらず、ネット上でRailsの事例をみません。private channelを使った事例は特に。
なお、Laravelでは事例をよく見ますし、すでに語り尽くされたのかも知れません。
が、僕は初めてだったので、改めてRailsで作りにはどうすればいいかといことで、やったのでその記録をのせます。
使用イメージ
使用手順
PUSHERのアカウントを作成
PUSHERの App keysで情報を入寮
.env_sample.env
ファイルを.env
ファイルにコピーして、PUSHERのApp keys の情報を入力する。インストール
rails db:migrate rails db:seed rails s
以下にアクセスする
http://localhost:3000ログインする。ブラウザを2つ開いて、それぞれ違うユーザーでログインして下さい。
ついになるようにチャットの相手を選んで下さい。(上述の動画参照のこと)
環境
- Rubyは 3.0.0
- dbは sqlite3
- Rails は 6.1
コードはこちら
Pusherを使うために追加したコードに集中して読みたい場合は以下のPRを参照下さい。
説明
- 擬似ログインを作る
- dot-envで設定を管理できるようにする
- Pusherにsign inして環境変数をセットする
- Pusherのprivateチャネルをsubscribeする
- チャットをできるようにする
擬似ログインを作る
本質では無いですが、ログインできた方がイメージが作りやすいように、擬似ログインを作りました。
特定のユーザーのuser_idをsessionに login_user_idとして保存します。
それにより、
current_user
authentificate_user!
のおなじみのメソッドを定義して参照できるようにしました。なので、deviseを使っているユーザであればとくやらないでもよいかと。
なお、作ってみるとこれ、以外と使い出があって個人的には満足しています。いろんなところで応用ききそうなので。
それっぽい、endpointを定義。
routes.rbget 'sign_in', to: 'sessions#new' post 'sign_in', to: 'sessions#create' get 'sign_out', to: 'sessions#destroy'それっぽいsessions_controllerを定義。
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new @users = User.all end def create @user = User.find(user_params[:id]) session[:login_user_id] = @user.id redirect_to root_path end def destroy session[:login_user_id] = nil redirect_to root_path end private def user_params params.require(:user).permit(:id) end enddot-envで設定を管理できるようにする
いつものあれです。dot-envです。とくに説明ないです。リンク貼っておきます。
Pusherにsign inして環境変数をセットする
Pusherです。アカウントを持っていない人はつくってください。 sign upしてください。
App keysのメニューに進んで、下図の情報を .envに転記して下さい。
(下記は例です・)
PUSHER_APP_ID = 1234567 PUSHER_KEY = 77777777777777777777 PUSHER_SECRET = 77777777777777777777 PUSHER_CLUSTER = ap3Pusherのprivateチャネルをsubscribeする
Pusher の Private channelstoAuthenticating usersが、非常に参考になります。これがあればOK。
該当のコードを下記にはります。
app/controllers/pusher_controller.rb# frozen_string_literal: true class PusherController < ApplicationController before_action :authenticate_user! def auth if current_user response = Pusher.authenticate(private_channel_name(current_user.id), params[:socket_id], { user_id: current_user.id # => required }) render json: response else render text: 'Forbidden', status: '403' end end end特に重要なのは、
private_channel_name(current_user.id)
private channel。
ここでユーザー毎のprivateチャネルを用意します。ここで用意したチャネルはprivateになります。他のユーザーはsubscribeできません。
app/controllers/application_controller.rbdef private_channel_name(user_id) "private-channel_user_id_#{user_id}" endprivateチャネルの subscribeは下記です。
740c9cda57f8f6c66b27'
ここには各自の app_key を入力して下さい。(secret_keyではないので間違えないように!)
'X-CSRF-Token': "<%= form_authenticity_token %>"
はCSRF tokenが入ります。app/views/chats/index.html.erblet pusher = new Pusher('<%= ENV['PUSHER_KEY'] %>', { authEndpoint: '/pusher/auth, cluster: 'ap3', encrypted: true, auth: { headers: { 'X-CSRF-Token': "<%= form_authenticity_token %>" } } }); let channel = pusher.subscribe("<%= @private_channel_name %>");チャットをできるようにする
チャットの送信は下記です。普通にpostのフォームです。
local: false, remote: true
が特徴です。app/views/chats/index.html.erb<%= form_with model: @chat, url: chats_path(with_user_id: @received_user.id), local: false, remote: true, id: 'chat-form' do |f| %> <%= f.text_field :message, autocomplete: 'off' %> <% end %>上記のpostを下記のcreateで処理します。
app/controllers/chats_controller.rbdef create @user = current_user @received_user = User.find(params[:with_user_id]) @chat = Chat.new(user: @user, received_user: @received_user, message: chat_params[:message]) if @chat.save! data = { id: @chat.id, message: @chat.message, user_name: @chat.user.name, user_id: @chat.user.id, created_at: @chat.created_at.to_s } Pusher.trigger_batch( [ { channel: private_channel_name(@user.id), name: event_name(@received_user.id), data: data }, { channel: private_channel_name(@received_user.id), name: event_name(@user.id), data: data } ] ) else # なにかエラー処理 end end重要なのは下記です。ここで、チャット送信者と受信者の両方のprivate チャネルへ通知をしています。
app/controllers/chats_controller.rbPusher.trigger_batch( [ { channel: private_channel_name(@user.id), name: event_name(@received_user.id), data: data }, { channel: private_channel_name(@received_user.id), name: event_name(@user.id), data: data } ] )上記を通知を下記で受信して、画面の描画を更新します。
event_nameの箇所には、やりとりしているユーザー独自のイベント名が入ります。
app/views/chats/index.html.erbchannel.bind("<%= @event_name %>", function (data) { let chat_message = data.message; let chat_created_at = data.created_at; let chat_user_name = data.user_name; let chat_user_id = data.user_id; let new_content = document.createElement('div'); new_content.innerHTML = ` <div> <span>「送信者」id=${chat_user_id} の ${chat_user_name}</span> <span>「送信日時」${chat_created_at}</span> <span>「メッセージ」${chat_message}</span> </div> ` let post_section_div = document.getElementById('post_section'); post_section_div.appendChild(new_content); let post_input = document.getElementById('chat_message'); post_input.value = ''; });event_nameはこんな感じ。user_idにはやりとりしている相手のuser_idがはいります。
app/controllers/application_controller.rbdef event_name(user_id) "new_post_with_user_id_#{user_id}" endチャット末尾に追加する。
app/views/chats/index.html.erblet post_section_div = document.getElementById('post_section'); post_section_div.appendChild(new_content);所感
使いやすいです。かなり簡単です。
ActionCableを使う時は Redisとセットだったりすることを考えると、Pusherの有料プランををつかってもいいのかなあーと思いました。
サーバーの心配せずに夜ぐっすり眠れそうなので。
- 投稿日:2021-03-21T18:31:16+09:00
rails - テストコード基礎(Rspec・FactoryBot)
テストコードの実装手順
Rspec・FactoryBotを使った基礎的なテストコード実装についてアウトプットもかねて自分用にまとめます。(今回はuserモデルの単体テストコード)
まずは実装の流れになります。
1.Gemインストール
2.Rspecの設定
3.FactoryBotの設定
4.テストコード記述ファイル作成
5.テストコード記述
6.テスト実行
1.Gemインストール
Gemfilegroup :development, :test do gem 'rspec-rails', '~> 4.0.0' #バージョン指定 gem 'factory_bot_rails' endターミナルbundle install2.Rspecの設定
Rspecのインストール、ディレクトリ・ファイルを作成する
ターミナルrails g rspec:installテストコードの結果をターミナル上に可視化する
.rspec--require spec_helper --format documentation #追加3.FactoryBotの設定
specディレクトリ内にfactoriesディレクトリ、user.rbを作成後
spec/factories/users.rbFactoryBot.define do factory :user do nickname {'test'} email {'test@com'} password {'111111'} password_confirmation {password} end end4.テストコード記述ファイル作成
ターミナルrails g rspec:model user5.テストコード記述
spec/models/user_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do before do @user = FactoryBot.build(:user) #インスタンス生成 end describe 'ユーザー新規登録' do it 'nicknameが空では登録できない' do @user.nickname = '' @user.valid? expect(@user.errors.full_messages).to include "Nickname can't be blank" end end end6.テスト実行
ターミナルbundle exec rspec spec/models/user_spec.rb
おまけ
エラーメッセージの日本語化
以下の手順でエラーメッセージを日本語にすることもできる。
1.Gemインストール
config/application.rbmodule Pictweet class Application < Rails::Application # 日本語の言語設定 config.i18n.default_locale = :ja #追加 end endGemfilegem 'rails-i18n'ターミナルbundle install2.各種ファイルの作成、記述
- config/localesディレクトリにdevise.ja.ymlファイルを作成
- devise-i18nから記述をコピーして貼り付け
config/locales/devise.ja.ymlja: activerecord: attributes: user: confirmation_sent_at: パスワード確認送信時刻 confirmation_token: パスワード確認用トークン confirmed_at: パスワード確認時刻 created_at: 作成日 current_password: 現在のパスワード current_sign_in_at: 現在のログイン時刻 current_sign_in_ip: 現在のログインIPアドレス email: Eメール 〜〜〜〜〜〜〜〜〜〜 以下略 〜〜〜〜〜〜〜〜〜〜〜〜〜〜
- config/localesディレクトリにja.ymlファイルを作成
config/locales/ja.ymlja: activerecord: attributes: user: nickname: ニックネーム tweet: text: テキスト image: 画像これで日本語化は完了です、あとはテストコードのエラーメッセージ部分を書き換えればテストも通ります。
- 投稿日:2021-03-21T17:48:41+09:00
【Rails】 dependent: :destroy とは
どんな機能なの?
結論から言うと、dependent: :destroyを使いことによって
親モデルのレコードを削除する際に、それに紐づく子モデルのレコードも同時に削除されます。具体的な使い方
dependent: :destroyはモデルのアソシエーションで使います。
例えば下記のようにユーザーとマイクロポストのアソシエーションを定義します。user.rbhas_many :mictropostsこれだけではユーザーが退会した時、そのユーザーのマイクロポストは残ってしまいます。
そのような場合に下記のようにdependent: :destroyを追加します。user.rbhas_many :mictroposts, dependent: :destroyこうすることでユーザー退会時には、そのユーザーのマイクロポストも同時に削除されます。
最後に
この機能は他に、いいね機能(いいねした投稿、された投稿が削除された場合、LikeModelのレコードも削除する。)やフォロー機能(フォローしたユーザー、されたユーザーが削除された場合、RelationshipModelのレコードも削除する。)など、アソシエーションをいじる場面でよく出てくるので必ず覚えておきたいですね!
また基本、応用関係なくなるほどと思ったことについて日記感覚で投稿していきます!
ありがとうございました!
- 投稿日:2021-03-21T17:42:35+09:00
RubyとRailsの個人的によく使うメソッドをまとめてみた
今回はタイトルにもある通り、個人的によく使うメソッドをまとめてみました。
初学者なので間違いがあれば指摘していただけると嬉しいです。そもそもメソッドとは?
メソッド = 処理。
- 自分で作るメソッド
- Rubyがあらかじめ用意してくれているメソッド
のふたつがある。
今回はRubyがあらかじめ用意してくれている便利メソッドを紹介します。putsメソッド
ターミナルに値を出力するメソッド。putsの後に値を記述することで出力できる。
puts "Helloworld" => Helloworldgetsメソッド
ターミナルに値の入力機能を起動するメソッド。入力された値は文字列としてプログラムに渡される。
また文字列として扱う際、getsで取得した値の後は、出力の際に改行され、gets.to_iで数値として扱う場合は改行されない。name = gets puts "こんにちわ、" + name + "さん!" => 好きな文字列を入力する(今回は太郎) => こんにちわ、太郎 さん!上記の通り、このままだと改行がされてしまうのでそんな時はchompメソッドを合わせて使うと良い。
chompメソッド
文字列の末尾に存在する改行を取り除くメソッド。
name = gets.chomp puts "こんにちわ、" + name + "さん!" => 好きな文字列を入力する(今回も太郎) => こんにちは、太郎さん!となって改行されるのを防いでくれる。
lengthメソッド(文字列)
文字列の文字数を数えるメソッド。その際、半角スペースも文字数として数えられる。
出力する際は数値として出力される。
a = "Helloworld".length puts a => 10ちなみにこんな感じで配列にくっつければ要素数を数えてくれる。
a = ["Hello","World"].length puts a => 2to_sメソッド
整数 → 文字列に変換するメソッド。
通常、整数(Integer)型と文字列(String)型による足し算はできないが、こうして型を換えることで可能になる。puts 20.to_s + "歳" => 20歳ただ、下記のように文字列に対して掛け算をすることは整数(Integer)型に変換しなくても可能。
puts "歳" * 10 => 歳歳歳歳歳歳歳歳歳歳歳to_iメソッド
文字列(String) → 整数(Integer)に変換するメソッド。
a = "10".to_i puts 20 + a => 30ちなみに文字列をto_iで変換し、整数型と四則演算すると、全て0として変換されるらしい。
冷静に考えたら当たり前なのだが、最近まで知らなかった。理想↓↓
a = "歳".to_i puts 20 + a => 20歳現実↓↓
a = "歳".to_i puts 20 + a => 20getsメソッドと合わせて使うと応用が効いて便利。
ちなみにgetsのsはstring(文字列)型のsらしいです。
(複数形だと思ってたなんて口が裂けても言えない....)input = gets.to_i puts 100 + input => 好きな数値を入力(今回は100を入力する) 200その他類似の変換系メソッド
個人的にはまだ活用できていないが、使い方を理解できると便利であろう変換系のメソッド。
メソッド 効果 to_f Floatクラス(浮動小数点数)に変換 to_sym シンボルに変換 to_h ハッシュオブジェクトに変換 to_a 配列オブジェクトに変換 requireメソッド
外部ファイルを読み込むためのメソッド。require "外部ファイル名"で読み込める。
ただしカレントディレクトリから読み込む場合はrequire "./外部ファイル名"で読み込む。日付や時間を取得する際に便利なメソッド
Dateクラス
Rubyの標準ライブラリの機能で日付を扱うためのクラスのこと。
require "date"という一文をコード内に記述することで使用できる。todayメソッド
本日の日付を取得するメソッド。
例)Date.today 今日の日付を取得するwdayメソッド
0~6の数値で曜日を取得するメソッド。
0:日曜日、1:月曜日、2:火曜日、3:水曜日、4:木曜日、5:金曜日、6:土曜日を表す。例)Date.today.wday 今日の曜日を数値で取得する
strftimeメソッド
日時データを指定したフォーマットで文字列に変換するメソッド。
例)message.created_at.strftime("%Y年%m月%d日 %H時%M分") 保存された日時を文字列で取得する
eachメソッド
配列や範囲オブジェクトなどで用意されているメソッドであり、オブジェクトに含まれている要素を順に取り出し、変数に格納します。
オブジェクト.each do |変数| 実行する処理1 実行する処理2 end↓↓↓↓
@posts = Post.all だとここでは認識しておいてください。
<% @posts.each do |post| %> タイトル:<% post.title %> 投稿者:<% post.user %> 投稿内容:<% post.product %> <% end %>each_with_indexメソッド
each(要素の繰り返し処理)と同時に、その要素が何番目に処理されたかを表すメソッド。デフォルトだと処理番号は0から割り当てられる。
僕はランキング機能を作成するさいに用いました。オブジェクト.each.with_index do |変数1,変数2| 実行する処理1 実行する処理2 end↓↓↓↓
<% @posts.each.with_index(1) do |post, i| %> 第<%= i %>位 <% post.title %> <% end %>@postsの中身と指定した数字(1)が |post, i|にそれぞれ格納され、「第◯位」と「投稿のタイトル」に割り振られる。
include?メソッド
指定した要素が配列や文字列内に含まれているかを判定するメソッド。
number = [100, 200, 300 , 400 , 500] puts number.include?(200) => truesplitメソッド
指定した区切り文字で対象となる文字列を分割して配列にする。
fruit = "apple,strawberry,grape,banana" puts fruit.split(',') => ["apple", "strawberry", "grape", "banana"]sliceメソッド
配列や文字列から指定した要素を取り出すことができるメソッド。
文字列の要素を指定する際は数字を用い、先頭の文字列は0から指定する。また第二引数として、取り出す要素の数も指定したい場合は指定することもできる。number = [100, 200, 300 , 400 , 500] puts number.slice(3,2) => [400 , 500]エクスクラメーションマーク(!)
末尾につけることにより破壊的メソッドになり、もとの配列や文字列を変化させるメソッド。
scanメソッド
対象の要素から引数で指定した文字列を数え、配列として返すメソッド。
puts "ワン!ワンッ!ワンワンッ!".scan("ワ") => [ワ, ワ, ワ, ワ]lengthと組み合わせると含まれている"ワ"の数で返ってくる。
puts "ワン!ワンッ!ワンワン!".scan("ワ").length => 4ただ、下記にcountというメソッドもあるのでそっちの方が使いやすい。
countメソッド
特定の文字列の中に指定した文字列がいくつ含まれているかを数えたり、配列の要素数を数えるメソッド。
puts "ワン!ワンッ!ワンワン!".count("ワ") => 4firstメソッド
配列から最初の要素を取得するメソッド。
引数がある場合は配列の最初の要素からその引数分の要素を配列で取得するname = ["太朗", "花子", "二郎"] puts name.first(2) => ["太朗", "花子"]lastメソッド
配列から最後の要素を取得するメソッド。
引数がある場合は配列の最後の要素からその引数分の要素を配列で取得するname = ["太朗", "花子", "二郎"] puts name.last(2) => ["花子", "二郎"]sortメソッド
配列の要素を一定の規則(アルファベット、数値の降順、昇順など)で並び替えるメソッド。
fruit = ["apple", "strawberry", "grape", "banana"] puts fruit.sort => ["apple", "banana", "grape", "strawberry"]joinメソッド
配列内の要素と要素を連結させるメソッド。
引数を渡すことで区切ることもできる。fruit = ["apple", "strawberry", "grape", "banana"] puts fruit.join(",") => apple,strawberry,grape,bananapushメソッド
配列の末尾に要素を連結する
fruit = ["apple", "strawberry", "grape", "banana"] puts fruit.push("orange") => ["apple", "strawberry", "grape", "banana","orange"]unshiftメソッド
配列の先頭に要素を追加する。
fruit = ["apple", "strawberry", "grape", "banana"] puts fruit.unshift("orange") => ["orange", "apple", "strawberry", "grape", "banana"]shiftメソッド
配列の先頭の要素を削除する。または配列の先頭の要素を取り出す。
fruit = ["apple", "strawberry", "grape", "banana"] puts fruit.shift => ["Apple"]pluckメソッド
引数に指定したカラムの配列を返す(Railsのメソッド)
#Fruitモデルのnameカラムに "Apple" "grape" "banana" が入っている場合 => Fruit.pluck(:name) => ["Apple","grape","banana"]最後に
まだまだ足りないメソッドだらけで、適切に使えているかどうかも定かではありませんが、使える技が増えればそれだけ自分の中のアイデアを形にすることができると思います。
また、思い付いたらこれから追加していく予定です。
誰かの手助けになれればいいなと思います。
間違いや指摘があればコメントに残していただけると幸いです。
- 投稿日:2021-03-21T16:54:15+09:00
【Rails6】多対多のアソシエーションを利用したグループ参加機能の実装
多対多のアソシエーションを利用してグループ参加機能を実装する方法を紹介します。
目的
- 前提条件
- グループ参加機能の仕様
- 動作確認
- 実装手順1:モデルの作成
- 実装手順2:コントローラーの作成
- 実装手順3:ルーティングの設定
- 実装手順4:ビューの作成
前提条件
Rails 6.0.3.5
グループ参加機能の仕様
承認制ではなく、グループに参加するボタンをクリックすると、データベースにユーザーIDとグループIDに組み合わせが保存されて参加できる簡単なものになります。
動作確認
- 「Join this group」をクリック
- Are you sure to join this group?」というメッセージが出る
- OKをクリックするとグループのトップページに移動する
- 「Notice: joined the group!!」のフラッシュメッセージが出る。
これでグループへの参加が完了です。
それでは説明していきます。
グループ参加機能の実装に関係する記述以外は省略しているのでご承知おきください。実装手順1:モデルの作成
用意するモデル(テーブル)はuser, group, user_groupの3つです。
app/models/user.rbclass User < ApplicationRecord has_many :user_groups has_many :groups, through: :user_groups endapp/models/group.rbclass Group < ApplicationRecord has_many :user_groups has_many :users, through: :user_groups endapp/models/user_group.rbclass UserGroup < ApplicationRecord belongs_to :user belongs_to :group validates :user_id, uniqueness: { scope: :group_id } end実装手順2:コントローラーの作成
groups_controller
とuser_groups_controller
を以下のように記述します。グループ参加機能の実装には、users_controller
への記述は特にありません。app/controllers/groups_controller.rbclass GroupsController < ApplicationController def show @group = Group.find(params[:id]) # UserGroupテーブルからログインユーザーと、詳細を表示しているグループの組み合わせを探します。 # 組み合わせがなければnilを返します。 @userGroup = UserGroup.find_by(user_id: current_user.id, group_id: params[:id]) endapp/controllers/user_groups_controller.rbclass UserGroupsController < ApplicationController def create @userGroup = UserGroup.new(user_id: current_user.id, group_id: params[:group_id]) if @userGroup.save # グループ参加後のリダイレクト先を指定 redirect_to XXXX_path(@userGroup.group_id), notice: 'joined the group!!' end end end実装手順3:ルーティングの設定
user_groups_controller.rb
でparams[:group_id]
をを使用するアクションを定義しているため、user_groupsコントローラー
をgroupsコントローラー
にネストさせます。config/routes.rbRails.application.routes.draw do resources :groups, only: :show do resources :user_groups, only: :create end end実装手順4:ビューの作成
ボタンの部分の記述だけ掲載します。
条件分岐で@userGroup
に値が入っていれば、グループに参加しているのでグループメンバーしか見られない情報を表示し、値が入っていなければで「Join the group」ボタンを表示します。app/views/groups/show.html.erb<% if @userGroup.present? %> <div class="form-group"> <div class="schedule-item"> <div class="schedule-item-header text-left"> <p class="bold">Task</p> </div> <div class="schedule-item-body text-left"> <p><%= @group.task %></p> </div> </div> </div> <% else %> <div class="actions"> <%# user_groupsコントローラーのcreateアクションを実行するリンク %> <%= link_to 'Join this group', group_user_groups_path(@group.id), method: :post, data: { confirm: 'Are you sure to join this group?'}, class:"sign-up-btn" %> </div> <% end %>これでグループ参加機能の実装完了です。
- 投稿日:2021-03-21T16:50:35+09:00
【Rails】古いデータベースが影響してエラーが出た話
概要
以前作成したRailsアプリを削除して、再び同じ名前で同様のアプリ作成しました。
そしてrails db:migrate
実行後、ActiveRecord::PendingMigrationError
が発生した。結論から言うと、過去アプリのデータベースが残っており
作成したアプリ上のdb/migrateディレクトリ内
のファイルをコマンドdb:migrate
にて反映させようとした際、ファイルのバージョンが異なるためエラーが発生しました。
開発環境
・データベース:PostgreSQL
・railsのバージョン:6.1.3
・PC:macbook proエラー発生から解決までの流れ
- 元々あったrailsアプリを削除する。(rails db:create,rails db migrate実行済み)
- 元々あったrailsアプリと同じ名前のrailsアプリを再び作成する。(rails db:create実行済み)
rails db:migrate
を実行後、rails s
にてサーバを立ち上げてブラウザを見ると、ActiveRecord::PendingMigrationError
が発生する。rails db:version
を実行すると、terminal上のファイルとアプリmigrationファイル(db/migrate)のバージョンが異なる。rails db:reset
を実行してrails db:migrate
を行うと、問題なくアプリケーションを実行できる。
また、rails db:version
にてファイルを確認すると、terminal上とrailsアプリのmigrationファイルが一致していることが確認できた。rails db:migrate
を行ってアプリを起動してみると、問題なく動作した。![]()
エラーが発生した原因
原因は私の理解不足によるもので、アプリファイルを削除すると一緒にデータベースも消すことができると思っていたためです。
(アプリファイルとデータベースは、同一の場所にあると思っていた)
実際は、下記ディレクトリにデータベースが保存されています。
また、データベース削除とversion確認コマンドは、下記になります。PostgreSQL:… /usr/local/var/postgres 内
MySQL:… /usr/local/var/mysql 内データベース削除:
rails db:reset
(データベース内の情報は消えるので注意!)
データベースversion確認:rails db:version
最後に
正直なところ、データベースについてまだまだわかっていない点がたくさんあります。
もし間違っている点等がある場合、ご指摘頂けると大変ありがたいです。
最後まで読んで頂きありがとうございました。
- 投稿日:2021-03-21T16:02:01+09:00
テストコード その2
テストコードを実装するための準備
復習として作成しました。
RspecのGemを導入する。
Gemfileのgroup :development, :test do~endの間に
gem 'rspec-rails', '~> 4.0.0'
と記述する。
このように記述するとGemの動作に制限を持たせることができる。
bundle install →rails g rspec:installの順でファイルを作成する。
.rspecファイルを開き、--format documentation
と記述する。
こう記述することでテストコードの結果を可視化する。
rails g rspec:model テストしたいモデル名
でモデルのテストファイルを作る。
これで準備OK。テストコードの雛形
describe
テストコードのグループ分けをする。ここでは主にどの機能に対してテストコードを行うかをグループ分けする。
例:Userモデルの新規登録機能についてテストコードを行うとなると以下のようになる。rbrequire 'rails_helper' RSpec.describe User, type: :model do describe '新規登録' do # 新規登録について記述する end endit
itはdescrideでグループ分けした機能でどのような状況のテストコードを行うか記述する。
上の# 新規登録について記述する がitに当たる。
形を整えるとこんな感じになる。rbit "新規登録ができる" do # 登録ができるテストコードを記述する endexample
itで小分けしたグループのことまたは、itに書かれた内容のことを差す。テストコードを実行するときは以下のコマンドをターミナルに打つ。
ターミナルbundle exec rspec spec/models/user_spec.rbbundle execでGemの依存関係を整理してくれる。多くのGemは他のGemと依存関係が成り立っているため整理する必要がある。
rspecでspecディレクトリ以下に書かれたテストコードを実行している。結果が緑色だったら成功となる。
テストコードに使われるメソッド
vaild?メソッド
通常validateが働くときは実際に入力して実行したときである。
なので任意のタイミングでバリデーションを実行させ、確認する。
エラーがあればfalseをなければtrueを返してくれる。expectation
テストコードを実行した結果が想定通りかどうか確認する。引数には検証で得られた実際の挙動を指定する。matcher
expectの引数と想定した挙動が一致しているか確認する。どのような挙動を想定しているか記述する。
■代表的なmatcher
・include
expectの引数にincludeの引数が含まれていることを確認する。
実際の検証結果がincludeの引数と同じであることとみる。
・eq
expectの引数とeqの引数が等しいことを確認する
例えばexcept(1+1).to eq(2)という感じ。valid?を実行し、falseが返ってきたときエラーの内容を確認するメソッド
errors
インスタンスにエラーを示す情報がある場合、その内容を返すメソッド
full_messages
エラー内容からエラーメッセージを配列として取り出し、エラーメッセージを表示するメソッド以上を踏まえて、Nameのvaild?がfalseだったときに想定することが起きるかどうかを確認したいコードは以下の通り
rbexpect(user.erroors.messages).to include("Name can't be blank")exceptで取り出したエラーメッセージがincludeに含まれたものであればテストは成功という事になる。
以上です。
- 投稿日:2021-03-21T15:37:59+09:00
投稿機能 統合テストの実装
今回ポートフォリオ作成中にでたエラーの解決策を備忘録も含め、同じところでつまづかれている方などいましたら、ご参考にして頂けましたら幸いです。
投稿機能では画像つきの機能を取り入れてまして、
でたエラーに関してが、Capybara::ElementNotFound: Unable to find field "post[images][]" that is not disabled from /Users/takuya/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/capybara-3.35.3/lib/capybara/node/finders.rb:303:in `block in synced_resolve'上記のエラーでした。
アウトプットの前に、
rspec
factory.bot
上記は導入済みで、進めさせていただきます。ちなみにかなり時間を取られました?
投稿機能統合テスト記述
RSpec.describe '写真テキスト投稿', type: :system do before do @user = FactoryBot.create(:user) @post_name = Faker::Lorem.sentence end context '写真テキスト投稿ができるとき'do it 'ログインしたユーザーは新規投稿できる' do # トップページに移動する visit root_path # トップページにログインページへ遷移するボタンがあることを確認する expect(page).to have_content('LOGIN') # ログインページへ遷移する visit new_user_session_path # 正しいユーザー情報を入力する fill_in 'Eメール', with: @user.email fill_in 'パスワード', with: @user.password # ログインボタンを押す find('input[name="commit"]').click # トップページへ遷移することを確認する expect(current_path).to eq(root_path) # 写真投稿一覧ページに移動する visit posts_path # 新規投稿ページへのリンクがあることを確認する expect(page).to have_content('PUSH 投稿') # 投稿ページに移動する visit new_post_path # フォームに情報を入力する fill_in 'post[images][]', with: "public/images/test_image.jpg" fill_in 'post[name]', with: @post_name # 送信するとPostモデルのカウントが1上がることを確認する expect{ find('input[name="commit"]').click }.to change { Post.count }.by(1) # 写真投稿一覧ページに移動する visit posts_path # 写真投稿一覧ページには先ほど投稿した内容の写真テキストが存在することを確認する(画像) expect(page).to have_selector("img[src$='test_image.png']") # 写真投稿一覧ぺージには先ほど投稿した内容の写真テキストが存在することを確認する(テキスト) expect(page).to have_content(@post_name) end end context '写真投稿ができないとき'do it 'ログインしていないと新規投稿ページに遷移できない' do # 写真投稿一覧ページに移動する visit posts_path # 新規投稿ページへのリンクがない expect(page).to have_no_content('nPUSH 投稿') end end end
fill_in 'post[images][]', with: "public/images/test_image.jpg"
の上にbinding.pry
を入れて、上記コードを入力すると画像が挿入できない状態でした。要素中にinputタグのname要素を入れ込んでいるのになぜエラーが出るのかと四苦八苦していました。
一度頭を整理して、
fill_in
意外にも画像挿入する方法はないかと調べたところ、
attach_file
というメソットが引っかかりました。
そこで
fill_in
を削除してattach_file
メソットの記述を行いました。変更前
# フォームに情報を入力する fill_in 'post[images][]', with: "public/images/test_image.jpg" fill_in 'post[name]', with: @post_name変更後
# フォームに情報を入力する attach_file('post[images][]', image_path, make_visible: true) fill_in 'post[name]', with: @post_name上記を記述変更を行い、今すぐにでも実装を行いたかったのですが、焦ってはいけませんでした
変更後の記述の中に
image_path
を代入していることから、
フォームに情報を入力する
の統合テストを行う前に、添付する画像を定義する記述を行いました。
そうしないとなんの画像を指定しているのかわからないためですそして、
# 添付する画像を定義する image_path = Rails.root.join('public/test_image.png') # フォームに情報を入力する attach_file('post[images][]', image_path, make_visible: true) fill_in 'post[name]', with: @post_nameフォーム情報を入力するの前に、画像を定義する記述を行い、無事テストが行える状況になりました。
実装を行うと、問題なく統合テストを行うことができました
完成コード
RSpec.describe '写真テキスト投稿', type: :system do before do @user = FactoryBot.create(:user) @post_name = Faker::Lorem.sentence end context '写真テキスト投稿ができるとき'do it 'ログインしたユーザーは新規投稿できる' do # トップページに移動する visit root_path # トップページにログインページへ遷移するボタンがあることを確認する expect(page).to have_content('LOGIN') # ログインページへ遷移する visit new_user_session_path # 正しいユーザー情報を入力する fill_in 'Eメール', with: @user.email fill_in 'パスワード', with: @user.password # ログインボタンを押す find('input[name="commit"]').click # トップページへ遷移することを確認する expect(current_path).to eq(root_path) # 写真投稿一覧ページに移動する visit posts_path # 新規投稿ページへのリンクがあることを確認する expect(page).to have_content('PUSH 投稿') # 投稿ページに移動する visit new_post_path # 添付する画像を定義する image_path = Rails.root.join('public/test_image.png') # フォームに情報を入力する attach_file('post[images][]', image_path, make_visible: true) fill_in 'post[name]', with: @post_name # 送信するとPostモデルのカウントが1上がることを確認する expect{ find('input[name="commit"]').click }.to change { Post.count }.by(1) # 写真投稿一覧ページに移動する visit posts_path # 写真投稿一覧ページには先ほど投稿した内容の写真テキストが存在することを確認する(画像) expect(page).to have_selector("img[src$='test_image.png']") # 写真投稿一覧ぺージには先ほど投稿した内容の写真テキストが存在することを確認する(テキスト) expect(page).to have_content(@post_name) end end context '写真投稿ができないとき'do it 'ログインしていないと新規投稿ページに遷移できない' do # 写真投稿一覧ページに移動する visit posts_path # 新規投稿ページへのリンクがない expect(page).to have_no_content('nPUSH 投稿') end end end画像挿入するだけのことでしたがかなり時間を使ってしまいました。
冷静に頭を整理して、根本のところから他に定義するメソットはないのという発想で成功することができました。
もし似たような内容で詰まっている方などいましたら、ご参考にして頂けましたら幸いです。
宜しくお願いします!!
- 投稿日:2021-03-21T15:23:13+09:00
rails generate がうまくいかない時
原因
gem のspringがエラーを起こしている。そもそもgem とは
・Rails4からデフォルトで入ったアプリケーションプリローダー。
・一部のタスクを前もって実行しておき、その後に必要なタスク時間を短縮する。
・アプリケーションをバックグラウンドで実行し続けてくれる。
・アプリーケーションコードを編集したら内部のリロードを自動的に実行する。主に開発をする上で開発者に余計なタイムロスを発生させないようにするためのパッケージ。
解決方法
バックグラウンド側で動いているspringを止めてしまう。
・spring stopこれでOK!
- 投稿日:2021-03-21T15:10:01+09:00
rails urlパス変更
コントローラー名とは違う命名でルーティングを指定したい!
そんなことが起きた時に使えるコマンドを紹介します。resourcesとネスト
Railsのルーティング記法の基本は、複数形のresourcesメソッドと単数形のresourceメソッドです。また、Railsのルーティングにはネストを含む多くのオプションがあり、自由度が飛躍的に高まっています。
始めに
resources :notifications, only: [:show]notificationコントローラーのshowアクションにと、ごく普通の記述です。
/notifications/:id
の部分を違う名前に変更したいと思い。
path
オプションを使用する。resources :notifications, only: [:show], :path => "articles"おわりに
些細なことでも疑問に思った点
解決した点など更新して行きます?
- 投稿日:2021-03-21T14:36:29+09:00
再新規登録はできません 勝手にredirectされます
初めてrailsでポートフォリを作りをしています。
index画面と新規登録画面を作って
deviseをインストールして
画面表示もindex、新規登録画面ともに表示され
新規登録1度行ない、その時入力した情報が
データベースにも保存されていたことを確認しました。嬉しくなって再度新規登録画面を開け再度、登録ようとしました。
何度も。。。
http://localhost:3000/users/sign_up
を開いても
http://localhost:3000/が開きます。何度も
エラーは出ていないのでターミナルを見て考えました。
Started GET "/users/sign_up" for ::1 at 2021-03-21 14:07:01 +0900 Processing by Devise::RegistrationsController#new as HTML User Load (36.8ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1 Redirected to http://localhost:3000/GET "/users/sign_up"でアクセスしましたが
devise::RegistrationsControllerによって
UserがLoadされSELECT `users`.* FROM `users` WHERE `users`.`id` = 1(SQL文)⇨id番号1番のuserなのでログインしているとして
Redirected to http://localhost:3000/元の画面(localhost:3000/)にリダイレクトされているとのことでした。
無駄なことをしていました。
早くログアウト機能つけよう。残念!
- 投稿日:2021-03-21T13:45:33+09:00
inquiry
TL;DR
文字列等価のチェックで
'hoge' == 'hoge'
という書き方以外の方法があります。
Rails AcctiveSupportにはStringオブジェクトの拡張でinquiryというメソッドが定義されています。word = 'hoge'.inquiry word # => 'hoge' word.hoge? # => true word.fuga? # => falseどう定義されてる?
active_support/core_ext/string/inquiry.rb
で定義されてます。lib/active_support/core_ext/string/inquiry.rbrequire "active_support/string_inquirer" class String def inquiry ActiveSupport::StringInquirer.new(self) end endでは
ActiveSupport::StringInquirer
は何者?次で定義されてます。
lib/active_support/string_inquirer.rbmodule ActiveSupport class StringInquirer < String ... def method_missing(method_name, *arguments) if method_name.end_with?("?") self == method_name[0..-2] else super end end def respond_to_missing?(method_name, include_private = false) method_name.end_with?("?") || super end end endmethod_missingの部分がinquiryメソッドの核になってます。
メソッド名の最後に?
がついている場合、そのメソッド名の?
を除いた部分がインスタンス名と一致するかの真偽値を返します。ちなみに...
method_missingは呼び出したメソッドが定義されていなかった時に呼び出されるRuby組み込みのメソッドです。一つ目の引数に呼び出しに失敗したメソッド名、その引数が第二引数に渡されます。
つまりこんな感じclass String def method_missing(method_name, *args) puts "method named #{method_name} is not defined!" end end 'hoge'.fuga # => method named fuga is not defined!またrespond_to_missing?も同じくRuby組み込みのメソッドです。method_missingを呼び出すに該当するメソッドがrespond_to?で呼び出された際に応答します。
class String def method_missing(method_name, *args) puts "method named #{method_name} is not defined!" end def respond_to_missing?(method_name, include_private) method_name.end_with?("?") || super end end 'hoge'.respond_to?(:fuga?) # => true 'hoge'.respond_to?(:piyo) # => false終わり
参考
- https://github.com/rails/rails/blob/main/activesupport/lib/active_support/string_inquirer.rb
- https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/string/inquiry.rb
- https://docs.ruby-lang.org/ja/latest/method/BasicObject/i/method_missing.html
- https://docs.ruby-lang.org/ja/latest/method/Object/i/respond_to_missing=3f.html
- 投稿日:2021-03-21T11:59:53+09:00
scope使い方(Rails)
modelの中にscopeを書く
複数使う場合はスコープで書いた方がコードが読みやすくなりますし、
単体テスト書くことが可能になるため積極的に使った方がいいです。item.rbscope :published, -> { where(private: true) }users_controller.rbdef show if !@user.me?(current_user) @items = @user.items.published else @items = @user.items end endモデルで
{ where(private: true) }
定義することで
コントローラの記述が少なくなります。おまけ
user.rbdef me?(user_id) id == user_id end
current_user
と@user
が一致しているか
モデルに書くことで可能になります。users_controll.rbif !@user.me?(current_user)
- 投稿日:2021-03-21T10:26:25+09:00
エラーメッセージを自分でカスタマイズする
バリデーションの内容を自分好みに表示させる方法です。備忘録として載せたいと思います。
項目ごとに記述しているのでスマートではないかもしれません!
※現在は勉強段階なので実際の現場でどう使われているかは分かりません。実装
エラーの記述部分だけ載せたいと思います
モデル
contact.rbclass Contact < ApplicationRecord validates :title, presence: { message: "タイトルを入力してください" } validates :body, presence: { message: "内容を入力してください" } validates :name, presence: { message: "名前を入力してください" } validates :email, presence: { message: "メールアドレスを入力してください" } endmessageに好きなメッセージを設定できます。
コントローラ
※newページのform_withではconfirmアクションに飛ぶように指定しています
contacts_controller.rbdef confirm @contact = Contact.new(contact_params) @contact.valid? if @contact.errors.present? return render "new" end end保存する時ならエラーの検証が実行されますが、今回は確認画面を挟んでいるためvalid?メソッドを使い自分でトリガーを引いてあげます。(=.errorsでエラー内容を参照できる)
(valid?:エラーの有無を判定するメソッド。正常であればtrueを返す。)valid?をしないとエラーメッセージが入ってきません(=.errorsが使えない)
rails console(下)で確認してみるとこんな感じで、valid?をせず.errosを実行すると@messagesの中にエラー文が入ってきません。2.6.3 :001 > contact = Contact.new() => #<Contact id: nil, title: nil, body: nil, name: nil, email: nil, created_at: nil, updated_at: nil> 2.6.3 :002 > contact.errors => #<ActiveModel::Errors:0x00007f44c8018ad8 @base=#<Contact id: nil, title: nil, body: nil, name: nil, email: nil, created_at: nil, updated_at: nil>, @messages={}, @details={}> 2.6.3 :003 > contact.valid? => false 2.6.3 :004 > contact.errors => #<ActiveModel::Errors:0x00007f44c8018ad8 @base=#<Contact id: nil, title: nil, body: nil, name: nil, email: nil, created_at: nil, updated_at: nil>, @messages={:title=>["タイトルを入力してください"], :body=>["内容を入力してください"], :name=>["名前を入力してください"], :email=>["メールアドレスを入力してください"]}, @details={:title=>[{:error=>:blank}], :body=>[{:error=>:blank}], :name=>[{:error=>:blank}], :email=>[{:error=>:blank}]}>ビュー
contact.html.erb<% if contact.errors.messages.key?(:title) %> <%= contact.errors.messages[:title].first %> <% end %> <% if contact.errors.messages.key?(:body) %> <%= contact.errors.messages[:body].first %> <% end %> <% if contact.errors.messages.key?(:name) %> <%= contact.errors.messages[:name].first %> <% end %> <% if contact.errors.messages.key?(:email) %> <%= contact.errors.messages[:email].first %> <% end %>contact.errors.messages.key?(:title) : エラーがあったら実行する(条件分岐)
contact.errors.messages[:title].first : エラーメッセージの表示内容下にあるrails consoleを参考にしてください。
contact.errors.messagesで全てのエラーメッセージを取得できます。なので.key?メソッド(ハッシュのkeyがあるか確認する。)を使い指定するカラムにエラーがあるか確かめ、エラーがあれば表示させるといった条件分岐になります。.firstの部分を記述しないと配列の中に入った状態で表示されてしまうので記述しています。2.6.3 :003 > contact.valid? => false 2.6.3 :004 > contact.errors => #<ActiveModel::Errors:0x00007f44c8018ad8 @base=#<Contact id: nil, title: nil, body: nil, name: nil, email: nil, created_at: nil, updated_at: nil>, @messages={:title=>["タイトルを入力してください"], :body=>["内容を入力してください"], :name=>["名前を入力してください"], :email=>["メールアドレスを入力してください"]}, @details={:title=>[{:error=>:blank}], :body=>[{:error=>:blank}], :name=>[{:error=>:blank}], :email=>[{:error=>:blank}]}> 2.6.3 :004 > contact.errors.messages => {:title=>["タイトルを入力してください"], :body=>["内容を入力してください"], :name=>["名前を入力してください"], :email=>["メールアドレスを入力してください"]} 2.6.3 :006 > contact.errors.messages.key?(:title) => true 2.6.3 :007 > contact.errors.messages[:title] => ["タイトルを入力してください"] 2.6.3 :008 > contact.errors.messages[:title].first => "タイトルを入力してください"参考サイト
- 投稿日:2021-03-21T10:20:13+09:00
Routing Error解決のヒント
始めに
アプリ開発にはエラーはつきものです。
まずエラーが出た時の対処として、エラー文を読み解くことも大切ですが、
より早くエラーを解決するためには以下を考えることが必要です。エラーが出てしまった☹︎
↓
①何をしたらエラーになったのか考える
↓
②エラー文を解読する(どこの記述がエラーなのか読み解く)
↓
③どこがどう間違えている可能性があるのか仮説を立てるRouting Error = ルーティングがおかしい
文のそのままの意味でルーティングが通常通りに機能していないエラー文です。タイトルの下には No route matches [HTTP] "URIパターン" の記載があり、エラーになっているHTTPとURIを教えてくれています。まずはそこを確認します。また、その以下の表には rails routesコマンドを行なった時と同じ一覧があるのでエラーになっているHTTP、URIパターンと比べることができます。
解決のヒント
①何をした時にエラーが起こっているのかエラー文にあるHTTPとURIを使って調べる
- 異なるHTTPがあれば、そこのルーティングが動いた時にエラーがあることがわかる
②ルーティングの記述が間違っていないか(routes.rbファイル)
- do~endは囲まれているのか
- resourceの書き方、スペルに間違いはないか
③viewのパスに間違いはないのか(form_with,like_to,aのリンク先など)
- リンクの飛ぶ先のパスはあっているのか
- 情報の受け渡しは行われているのか
- スペル間違いはないか(@やsのつけ忘れ)
- 投稿日:2021-03-21T09:50:15+09:00
MySQLを使ったRailsアプリをHerokuにデプロイしようとしてエラーになった話
開発環境
- Ruby(3.0.0)
- Ruby on Rails(6.1.3)
- MySQL(8.2.3)
エラーになった背景
heroku run rails db:migrate上記のコマンドを入力した時に、下記のエラーが発生。
Mysql2::Error::ConnectionError: Can’t connect to local MySQL server through socket ‘/tmp/mysql.sock’ (2)エラーの原因
HerokuのデフォルトのデータベースがPostgresqlであるため、MySQLを使う場合は下準備が必要であるため。
解決方法
ClearDBというアドオンを使う
heroku addons:create cleardb:ignite上記のコマンドを入力します。
すると、ClearDBというアドオンを使うことができるようになります。Mysqlへの接続
次に、Rails側でそのMySQLデータベースの情報を知る必要がありますが、Railsは
DATABASE_URLという環境変数が設定されていると、その情報を使ってデータベースに接続するという仕様になっているため、
heroku config:add DATABASE_URL='mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true'でMySQLの情報を設定してあげると、その情報を使ってMySQLに接続できる、ということになります。
- 投稿日:2021-03-21T08:18:43+09:00
画像がアップデートされない。。
refile(Gem)を使って画像投稿をしていて、
マイページでプロフィール画像の編集できるようにしたが、
なぜか画像が更新されない。editアクション、updateアクションもやっている。
def edit @user = User.find(params[:id]) end def update @user = User.find(params[:id]) @user.update(user_params) redirect_to user_path(@user.id) endviewもこんな感じ
<%= attachment_image_tag @user, :profile_image, :fill, 60, 60, class: "img-circle pull-left profile-thumb", fallback: "no_image.jpg"%>
環境
・docker
・Rails試したこと
・ブラウザにキャッシュされているのかな?と思いキャッシュを削除。
・fillの部分をsizeに変更してみましたが出来ない。解決
docker-compose runをしてみたら、なぜか出来た。
⇨新しいコンテナが作られたからかな?
⇨最初、docker-copose up -d で放置していて、それから新しいコンテナを作っていなかったから?
⇨まだ頭のモヤモヤが。。。とりあえず、解決したので、進みます。もやもやがありますが、、、
- 投稿日:2021-03-21T08:10:03+09:00
Reactとaxiosを用いて非同期でサインアウトする
実装方針
- devise-auth-tokenで使用する、uid, client, access-tokenの3つが全て存在する場合のみサインアウトする
- csrf-tokenのチェックを事前に行う
- client, tokenをdestroyアクション内で削除
- csrf-token,sessionをafter_actionで削除
フロントエンド
リクエストの送信
getCsrfToken() { if (!(axios.defaults.headers.common['X-CSRF-Token'])) { return ( document.getElementsByName('csrf-token')[0].getAttribute('content') ) } else { return ( axios.defaults.headers.common['X-CSRF-Token'] ) } }; setAxiosDefaults() { axios.defaults.headers.common['X-CSRF-Token'] = this.getCsrfToken(); }; userAuthentification() { if (axios.defaults.headers.common['uid'] && axios.defaults.headers.common['client'] && axios.defaults.headers.common['access-token']) { axios.defaults.headers.common['uid'] axios.defaults.headers.common['client'] axios.defaults.headers.common['access-token'] } else { return null } } if (this.props.content == 'SignOut') { this.setAxiosDefaults(); this.userAuthentification() axios .delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})this.setAxiosDefaults(); this.userAuthentification()この2行でそれぞれcsrf-tokenとuser情報をrequest.headersにセットする。後者は新規登録時に発行される情報で、devise-auth-tokenを用いると発行されるもの。
def update_auth_header @token = @user.create_token return unless @user && @token.client @token.client = nil unless @used_auth_by_token if @used_auth_by_token && !DeviseTokenAuth.change_headers_on_each_request auth_header = @user.build_auth_header(@token.token, @token.client) response.headers.merge!(auth_header) else unless @user.reload.valid? @user = @user.class.find(@user.to_param) unless @user.valid? raise DeviseTokenAuth::Errors::InvalidModel, "Cannot set auth token in invalid model. Errors: #{@resource.errors.full_messages}" end end refresh_headers end endこれを新規登録後に読み込むことで request.headerにclient、tokenをmerge。uidはデフォルトでresponse.headersに含まれている。
.delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})ここは普段どおりのaxiosのリクエスト。パラメータにはuserの識別子となるuidを渡す。
サーバーサイド
def destroy @user = User.find_for_database_authentication(uid: request.headers['uid']) @token = request.headers['access-token'] @client = request.headers['client'] if @user && @client && @token @token.clear @client = nil render_destroy_success else render_destroy_error end endリクエストヘッダーからuid(email)を取り出してインスタンスをDBから参照し、token、clientもそれぞれリクエストヘッダーから定義する。
user,token,client全てが存在しないとログアウトできないようにしている。
次にtokenとclientを削除することでクライアント側でuserが存在しない状態にし、ログアウト処理完了。destroyアクション後にafter_actionで以下を読み込む
after_action :set_csrf_token_header after_action :reset_session, only: [:destroy]これでcsrf-tokenとsessionを削除
挙動
感想
結局deviseのコントローラー0から改造することになってしまった。
まだ処理内容わかっていない点も多いのでこの後コメントアウトをつける作業に移る。参考
devise-auth-token公式
https://github.com/lynndylanhurley/devise_token_auth/blob/master/app/controllers/devise_token_auth/sessions_controller.rb次やること
- 結合テスト
- コメントアウトつける
- react-routerの導入
- 投稿日:2021-03-21T08:10:03+09:00
【Rails】Reactとaxiosを用いて非同期でdevise-auth-tokenを利用したのサインアウトを行う
実装方針
- devise-auth-tokenで使用する、uid, client, access-tokenの3つが全て存在する場合のみサインアウトする
- csrf-tokenのチェックを事前に行う
- client, tokenをdestroyアクション内で削除
- csrf-token,sessionをafter_actionで削除
フロントエンド
リクエストの送信
getCsrfToken() { if (!(axios.defaults.headers.common['X-CSRF-Token'])) { return ( document.getElementsByName('csrf-token')[0].getAttribute('content') ) } else { return ( axios.defaults.headers.common['X-CSRF-Token'] ) } }; setAxiosDefaults() { axios.defaults.headers.common['X-CSRF-Token'] = this.getCsrfToken(); }; userAuthentification() { if (axios.defaults.headers.common['uid'] && axios.defaults.headers.common['client'] && axios.defaults.headers.common['access-token']) { axios.defaults.headers.common['uid'] axios.defaults.headers.common['client'] axios.defaults.headers.common['access-token'] } else { return null } } if (this.props.content == 'SignOut') { this.setAxiosDefaults(); this.userAuthentification() axios .delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})this.setAxiosDefaults(); this.userAuthentification()この2行でそれぞれcsrf-tokenとuser情報をrequest.headersにセットする。後者は新規登録時に発行される情報で、devise-auth-tokenを用いると発行されるもの。
def update_auth_header @token = @user.create_token return unless @user && @token.client @token.client = nil unless @used_auth_by_token if @used_auth_by_token && !DeviseTokenAuth.change_headers_on_each_request auth_header = @user.build_auth_header(@token.token, @token.client) response.headers.merge!(auth_header) else unless @user.reload.valid? @user = @user.class.find(@user.to_param) unless @user.valid? raise DeviseTokenAuth::Errors::InvalidModel, "Cannot set auth token in invalid model. Errors: #{@resource.errors.full_messages}" end end refresh_headers end endこれを新規登録後に読み込むことで request.headerにclient、tokenをmerge。uidはデフォルトでresponse.headersに含まれている。
.delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})ここは普段どおりのaxiosのリクエスト。パラメータにはuserの識別子となるuidを渡す。
サーバーサイド
def destroy @user = User.find_for_database_authentication(uid: request.headers['uid']) @token = request.headers['access-token'] @client = request.headers['client'] if @user && @client && @token @token.clear @client = nil render_destroy_success else render_destroy_error end endリクエストヘッダーからuid(email)を取り出してインスタンスをDBから参照し、token、clientもそれぞれリクエストヘッダーから定義する。
user,token,client全てが存在しないとログアウトできないようにしている。
次にtokenとclientを削除することでクライアント側でuserが存在しない状態にし、ログアウト処理完了。destroyアクション後にafter_actionで以下を読み込む
after_action :set_csrf_token_header after_action :reset_session, only: [:destroy]これでcsrf-tokenとsessionを削除
挙動
感想
結局deviseのコントローラー0から改造することになってしまった。
まだ処理内容わかっていない点も多いのでこの後コメントアウトをつける作業に移る。参考
devise-auth-token公式
https://github.com/lynndylanhurley/devise_token_auth/blob/master/app/controllers/devise_token_auth/sessions_controller.rb次やること
- 結合テスト
- コメントアウトつける
- react-routerの導入