20210608のRailsに関する記事は29件です。

[Ruby] 階層の深いHashのmerge(deep_merge)と、そのコード簡略化

困ったこと Hash{}のmergeメソッドが、同一階層内の別keyデータを保持してくれない。 irb(main):001:0> h = {a:{aa:11, ab:11}} irb(main):002:0> h.merge({a:{aa:99}}) ==>{:a=>{:aa=>99}} ==> {:a=>{:aa=>99, :ab=>11}} になってほしい TL;DR ↓ソースをコピペすればOK class Hash def deep_merge(*others) self.clone.deep_merge!(*others) end def deep_merge!(*others) _child_merge = -> (_, self_val, other_val){ if self_val.instance_of?(Hash) && other_val.instance_of?(Hash) self_val.merge other_val, &_child_merge else other_val end } self.merge! *others, &_child_merge end end ↓使用例 $ irb irb(main):001:1* class Hash irb(main):002:2* def deep_merge(*others) irb(main):003:2* self.clone.deep_merge!(*others) irb(main):004:1* end irb(main):005:1* irb(main):006:2* def deep_merge!(*others) irb(main):007:2* _child_merge = -> (_, self_val, other_val){ irb(main):008:3* if self_val.instance_of?(Hash) && other_val.instance_of?(Hash) irb(main):009:3* self_val.merge other_val, &_child_merge irb(main):010:3* else irb(main):011:3* other_val irb(main):012:2* end irb(main):013:1* } irb(main):014:1* self.merge! *others, &_child_merge irb(main):015:0* end irb(main):016:-> end ==>:deep_merge! irb(main):017:0> h = {a:{aa:11, ab:11}} irb(main):018:0> h.deep_merge({a:{aa:99}}) #deep_mergeは元データを変更しない ==>{:a=>{:aa=>99, :ab=>11}} irb(main):019:0> h ==>{:a=>{:aa=>11, :ab=>11}} irb(main):020:0> h.deep_merge!({a:{aa:99}}) #deep_merge!は元データも変更する ==>{:a=>{:aa=>99, :ab=>11}} irb(main):021:0> h ==>{:a=>{:aa=>99, :ab=>11}} 説明 どうやってソースを組み立てたかと、コード簡略化について説明していきます。 困ったことの解消 まずHashのmergeメソッドについて見てみます。 このメソッドは、同一階層内でなければ、複数の値を保持してくれます。 irb(main):001:0> h1 = { "a" => 100, "b" => 200 } irb(main):002:0> h2 = { "c" => 300, "d" => 400 } irb(main):003:0> h1.merge(h2) ==>{"a"=>100, "b"=>200, "c"=>300, "d"=>400} また以下のように、 keyが重複した場合のみ、merge元データとmerge先データでイテレータ処理ができます。 irb(main):031:0> h1 = { "a" => 100, "b" => 200 } irb(main):032:0> h2 = { "b" => 246, "c" => 300 } irb(main):033:0> h1.merge(h2) {|key, oldval, newval| newval - oldval} ==>{"a"=>100, "b"=>46, "c"=>300} そして、イテレータのなかでは1階層落ちたデータとなります。 irb(main):034:0> h1 = { "a" => 100, "b" => {"bb"=>210, "bc"=>220} } irb(main):035:0> h2 = { "b" => {"bd"=>230}, "c" => 300 } irb(main):036:1* h1.merge(h2) {|key, oldval, newval| irb(main):037:1* p oldval irb(main):038:1* p newval irb(main):039:0> } {"bb"=>210, "bc"=>220} {"bd"=>230} つまり、「keyが重複した場合」にイテレータ内で「再びmerge」すれば、 階層が深くなっても値を落とさずに処理できます。 irb(main):040:0> h1 = { "a" => 100, "b" => {"bb"=>210, "bc"=>220} } irb(main):041:0> h2 = { "b" => {"bd"=>230}, "c" => 300 } irb(main):042:0> h1.merge(h2) {|key, oldval, newval| oldval.merge(newval)} ==>{"a"=>100, "b"=>{"bb"=>210, "bc"=>220, "bd"=>230}, "c"=>300} # データが保持されてる! やりたかった処理(=同一階層内の別keyデータを保持)が 実現できるメドが立ちました。 再帰処理の追加 ただ1階層ではダメで、階層を深堀りしていく必要があります。 そこで、処理をメソッド化し、 メソッド内部で、再帰的にイテレータ処理を行うようにします。 また、再帰的に処理を行うなかで、片方がHashでない場合は、 merge先データ(=newval)を正として上書きするので、イテレータ処理は不要となります。 これらを反映するとこうなります。 irb(main):043:1* def child_merge(key, oldval, newval) irb(main):044:2* if oldval.instance_of?(Hash) && newval.instance_of?(Hash) irb(main):045:2* return oldval.merge(newval) {|key, oldval, newval| child_merge(key, oldval, newval)} irb(main):046:2* else irb(main):047:2* return newval irb(main):048:1* end irb(main):049:0> end ==>:child_merge irb(main):050:0> h1 = { "a" => 100, "b" => {"bb"=>{"bbb"=>210}, "bc"=>220} } irb(main):051:0> h2 = { "b" => {"bb"=>{"bbe"=>240}, "bd"=>230}, "c" => 300 } irb(main):052:0> h1.merge(h2) {|key, oldval, newval| child_merge(key, oldval, newval)} ==>{"a"=>100, "b"=>{"bb"=>{"bbb"=>210, "bbe"=>240}, "bc"=>220, "bd"=>230}, "c"=>300} リファクタ ここですこし、リファクタをします。 簡易な処理なのでreturnを省略 def child_merge(key, oldval, newval) if oldval.instance_of?(Hash) && newval.instance_of?(Hash) oldval.merge(newval) {|key, oldval, newval| child_merge(key, oldval, newval)} else other_val end end 使わないキーは_にして省略 def child_merge(_, oldval, newval) この変数3つ|key, oldval, newval|が長いので、lambda関数に変更 (意外とこの、"引数"+"ブロック処理"のlambda記法が見つからなくて、めちゃくちゃ探しました) child_merge = -> (_, oldval, newval){ if oldval.instance_of?(Hash) && newval.instance_of?(Hash) oldval.merge newval, &child_merge else newval end } けっこうシンプルになりました。 Hashクラスへの適用 最後に、(Rubyの大きな特徴のひとつですが)classにメソッドを直接overrideできるので、 そちらを利用して、よりコードをシンプルにしていきます。 メソッドの変数名も、Hash#mergeに合わせて self_val, other_valにします。 class Hash def deep_merge(other_h) _child_merge = -> (_, self_val, other_val){ if self_val.instance_of?(Hash) && other_val.instance_of?(Hash) self_val.merge other_val, &_child_merge else other_val end } self.merge other_h, &_child_merge end end (private変数は_xxxにしてます。ruby的には意味ないです。ただの好みです) また、Hash#mergeは複数ハッシュも対応しているので、その対応も追加します。 class Hash def deep_merge(*others) (省略) self.merge *others, &_child_merge end end 元データも変更する、Hash#merge!にも対応します。 class Hash def deep_merge!(*others) (省略) self.merge! *others, &_child_merge end end そして最後に、通常のdeep_mergeも追加して、完成です。 class Hash def deep_merge(*others) self.clone.deep_merge!(*others) end def deep_merge!(*others) _child_merge = -> (_, self_val, other_val){ if self_val.instance_of?(Hash) && other_val.instance_of?(Hash) self_val.merge other_val, &_child_merge else other_val end } self.merge! *others, &_child_merge end end 使用方法 TL;DRの使用例にもあるように、 Hashクラスの変数であれば、メソッドを直接呼び出せるようになります。 irb(main):001:1* class Hash (省略) irb(main):016:-> end ==>:deep_merge! irb(main):017:0> h = {a:{aa:11, ab:11}} irb(main):018:0> h.deep_merge({a:{aa:99}}) ==>{:a=>{:aa=>99, :ab=>11}} irb(main):019:0> h ==>{:a=>{:aa=>11, :ab=>11}} irb(main):020:0> h.deep_merge!({a:{aa:99}}) ==>{:a=>{:aa=>99, :ab=>11}} irb(main):021:0> h ==>{:a=>{:aa=>99, :ab=>11}} irb(main):022:0> h.deep_merge({a:{ac:11}}, {b:1}) ==>{:a=>{:aa=>99, :ab=>11, :ac=>11}, :b=>1} irb(main):023:0> h.deep_merge({a:{ac:11}}, {b:1}, {b:9}) ==>{:a=>{:aa=>99, :ab=>11, :ac=>11}, :b=>9} 以上。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails API + Vue.js のSPAにCarrierWavewo導入する

はじめに CarrierWaveの導入はRailsアプリケーションでは簡単にできるのですが、SPAでは少し苦労したのでまとめておきます。 本記事では、ローカル(public/uploader以下)にファイルをアップロードするデフォルトの設定で進めます。 また、CarrierWaveの基本的な説明は省略します。 環境 Ruby 3.0.0 Rails 6.1.3 Vue.js 2.6.12 Vue CLI 4.5.1 CarrierWaveのインストール この記事ではJSONデータの作成にjbuilderを使うので、一緒にインストールしておきます。 Gemfile gem 'carrierwave' gem 'jbuilder' $ bundle install Railsの設定 $ rails g uploader Image $ rails g model Post image:string $ rails db:migrate app/models/post.rb mount_uploader :image, ImageUploader config/routes.rb Rails.application.routes.draw do scope format: 'json' do resources :posts end end app/uploaders/image_uploader.rbはデフォルト設定のまま使います。 ただし、このままファイルをアップロードしてVue.js側で取得するとURLは "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"となり表示できません。 なので、config/initializers/carrierwave.rbを作成して、 config/initializers/carrierwave.rb CarrierWave.configure do |config| config.asset_host = 'http://localhost:3000' end これで、"http://localhost:3000/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"となり表示できるようになります。 続いて、Postsコントローラーを作ります。 app/controllers/posts_controller.rb class PostsController < ApplicationController def index posts = Post.all @images = posts.map { |post| post.image.url } end def create @post = Post.new(post_params) if @post.save render json: :created else render json: @post.errors, status: :unprocessable_entity end end private def post_params params.require(:post).permit(:image) end end ポイントはpost.image.urlです。 Rails側でurlまで取得しておかないと、Vue.jsで表示したときにconsoleに警告が出ます。 app/views/posts/index.json.jbuilderを作成してRails側は完了です。 app/views/posts/index.json.jbuilder json.images do json.array! @images end Vue.jsの設定 まずは、投稿フォームを作ります。 <input type="file">ではv-modelは使えないので、@￰changeでファイルを取得します。 PostsNew.vue <template> <div> <input type="file" @change="setImage"> <button @click="postImage"> </div> </template> <script> import axios from '@/axios' export default { data () { return { image: '' }, methods: { setImage (e) { this.image = e.target.files[0] }, postImage () { const formData = new FormData() formData.append('post[image]', this.image) axios.post('/posts', formData) } } } </script> Rails側でStrong Parametersを設定しているので、パラメータを {"post" => {"image"=>"xxxxx/xxxx/xxxx"}} で渡すために、formData.append('post[image]', this.image)としています。 最後に投稿一覧を作成します。 PostsIndex.vue <template> <div v-for="(image, index) in images" :key="index"> <img :src="image"> </div> </template> <script> import axios from '@/axios' export default { data () { return { images: [] }, created () { axios.get('/posts').then(response => { this.images = response.data.images }) } } </script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails API + Vue.js のSPAにCarrierWaveを導入する

はじめに CarrierWaveの導入はRailsアプリケーションでは簡単にできるのですが、SPAでは少し苦労したのでまとめておきます。 本記事では、ローカル(public/uploader以下)にファイルをアップロードするデフォルトの設定で進めます。 また、CarrierWaveの基本的な説明は省略します。 環境 Ruby 3.0.0 Rails 6.1.3 Vue.js 2.6.12 Vue CLI 4.5.1 CarrierWaveのインストール この記事ではJSONデータの作成にjbuilderを使うので、一緒にインストールしておきます。 Gemfile gem 'carrierwave' gem 'jbuilder' $ bundle install Railsの設定 $ rails g uploader Image $ rails g model Post image:string $ rails db:migrate app/models/post.rb mount_uploader :image, ImageUploader config/routes.rb Rails.application.routes.draw do scope format: 'json' do resources :posts end end app/uploaders/image_uploader.rbはデフォルト設定のまま使います。 ただし、このままファイルをアップロードしてVue.js側で取得するとURLは "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"となり表示できません。 なので、config/initializers/carrierwave.rbを作成して、 config/initializers/carrierwave.rb CarrierWave.configure do |config| config.asset_host = 'http://localhost:3000' end これで、"http://localhost:3000/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"となり表示できるようになります。 続いて、Postsコントローラーを作ります。 app/controllers/posts_controller.rb class PostsController < ApplicationController def index posts = Post.all @images = posts.map { |post| post.image.url } end def create @post = Post.new(post_params) if @post.save render json: :created else render json: @post.errors, status: :unprocessable_entity end end private def post_params params.require(:post).permit(:image) end end ポイントはpost.image.urlです。 Rails側でurlまで取得しておかないと、Vue.jsで表示したときにconsoleに警告が出ます。 app/views/posts/index.json.jbuilderを作成してRails側は完了です。 app/views/posts/index.json.jbuilder json.images do json.array! @images end Vue.jsの設定 まずは、投稿フォームを作ります。 <input type="file">ではv-modelは使えないので、@￰changeでファイルを取得します。 PostsNew.vue <template> <div> <input type="file" @change="setImage"> <button @click="postImage"> </div> </template> <script> import axios from '@/axios' export default { data () { return { image: '' }, methods: { setImage (e) { this.image = e.target.files[0] }, postImage () { const formData = new FormData() formData.append('post[image]', this.image) axios.post('/posts', formData) } } } </script> Rails側でStrong Parametersを設定しているので、パラメータを {"post" => {"image"=>"xxxxx/xxxx/xxxx"}} で渡すために、formData.append('post[image]', this.image)としています。 最後に投稿一覧を作成します。 PostsIndex.vue <template> <div> <div v-for="(image, index) in images" :key="index"> <img :src="image"> </div> </div> </template> <script> import axios from '@/axios' export default { data () { return { images: [] }, created () { axios.get('/posts').then(response => { this.images = response.data.images }) } } </script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActionMailboxでRSpecのテストを書く

この記事の内容 メール受信周りの機能を実装中、RSpecでActionMailboxのテストを書くことになりました。 RailsガイドやパーフェクトRuby on RailsにはMiniTestの事例しか載っておらず、日本語の資料も少なかったので手順をメモしておきます。 ただし、ActionMailboxの実像そのものは他に良い記事がたくさんあるので、本記事ではあくまでもRSpecによるテスト記載について取り扱っています。 前提 使用したRailsとRSpecのバージョンは下記の通りです。 Rails 6.0.0 RSpec 5.0.1 また、テスト対象のMailBox内ではこのようにprocessメソッドを使ってメール送信処理を実装しています。 examples_mailbox.rb class ExamplesMailbox < ApplicationMailbox before_processing :set_organization def process # ... end end 準備 rspec-railsのバージョンをRails6系対応にする ActionMailboxがRails6の機能なので、RSpecもRails6に対応したものにする必要があります(←ここに気づかなくて結構ハマった)。 RSpecのGithubのReadmeによるとRails6系にはRSpec5系が必要なようなので、RSpecをアップデート合わせます。 bundle update rspec 依存するGemのアップデートが必要だと出たら、それもアップデートしましょう。 RSpecでActionMailbox::TestHelperを使えるようにする Rails(Mini Test)の機能である、ActionMailbox::TestHelperをRSpecでも使いたいので、spec/rails_helper.rbに以下のように記載します。 spec/rails_helper.rb require 'action_mailbox/test_helper' RSpec.configure do |config| config.include ActionMailbox::TestHelper, type: :mailbox end これで、 type: :mailboxのテストの際にActionMailbox::TestHelperのメソッドが使えるようになります。 実装 実際に書いたコードは概ね下記の通りです。 spec/mailboxes/examples_mailbox_spec.rb require 'rails_helper' RSpec.describe ExamplesMailbox, type: :mailbox do it "@your-domain-name.jpあてのメールを受信できる" do expect(ExamplesMailbox) .to receive_inbound_email(to: "sample@your-domain-name.jp") end context 'メールが送信された時' do let!(:mail) { Mail.new( from: 'replies@example.com', to: 'sample@your-domain-name.jp', subject: "メールのタイトル" ) } it 'logが一件生成される' do expect{ process(mail) }.to change(Log, :count).by(1) end # 以下略 ... end end processはRSpecのActionMailboxのメソッドで、RSpec4系以上で利用できます。 その他、色々なMailbox用のRSpecが同じページに載っていて、便利でした。 Action mailbox spec 感想、参考文献 以上で実装完了です。 実装時にはかなり苦労しましたが、改めて書き直してみるとなんてことなかったですね。。。。 実装にはこちらの記事の記述が丁寧で役立ちました! How to Route Emails with Action Mailbox
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

いいねボタンの応答速度を速く見せる方法

初めに いいねボタンの色変更ってそのままだと結構時間かかりますよね。 今回はjqueryを使いボタンを押した瞬間に色が変わるようにしようと思います。 ※アニメーションはやりません。 実装 _like.html.slim - if current_user.already_liked?(post) = link_to "/posts/#{post.id}/likes/#{post.likes.ids}", remote: true, method: :delete do <i class="fas fa-heart unlike font-13"></i> - else = link_to post_likes_path(post), remote: true, method: :post do <i class="far fa-heart like font-13"></i> span.ma-right-5 = post.likes.pluck(:id).length 普通はこうやってajaxのいいねボタンを実装すると思いますが、これだと ボタンを押す→Likeデータ作成→create.js.erb→ボタンの色変更 という処理になります。 でもこれだとLikeデータを作成してからajaxなので更新に明らかに間ができます。 なのでボタンを押した瞬間に色を変えて、裏でlikeデータを作成するようにします。 _like.html.slim - if current_user.already_liked?(post) = link_to "/posts/#{post.id}/likes/#{post.likes.ids}", remote: true, method: :delete do span.unlike <i class="fas fa-heart font-13"></i> - else = link_to post_likes_path(post), remote: true, method: :post do span.like <i class="far fa-heart font-13"></i> span.ma-right-5 = post.likes.pluck(:id).length javascript: $('.like').click(function() { $(this).delay(1).queue(function(){ $(this).html('<i class="fas fa-heart unlike font-13"></i>'); }); }) $('.unlike').click(function() { $(this).delay(1).queue(function(){ $(this).html('<i class="far fa-heart like font-13"></i>'); }); }) こんな感じです。 まとめ 現在開発中のSNS、Ammotのいいねボタンがかなり遅くて悩んでいたのですが、案外簡単に解決できてよかったです。 よかったらAmmotも見てみてください。 ↓トップページ: ↓僕のアカウント
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sequel ProでインポートしたらMysql2::Error: Incorrect string value

Sequel Pro便利です。 視覚的にデータベースを確認できるのは大きい。 そしてCSVでもインポート可能で、直感的。 インポート方法はこちらから Sequel Proを使ったMySQLのテーブルデータ追加方法 注意点はidも入力しなきゃいけないというところですかね。 あとは日付の項目の型ですかね。 私の場合 yyyy-mm-dd hhss という形でした。  発生したエラーはMysql2::Error: Incorrect string value 今回の場合はarchivesというテーブルでarchivetitleでエラーが発生 確認したところ、文字化けしているようでしたが、原因は絵文字。 【Short video】?NENE's GREETING?【桃鈴ねね/ホロライブ】 という具合でガンガン絵文字が入っているのでエラーが発生。 rails db:drop で一度データベースを削除 database.ymlを修正 default: &default adapter: mysql2 charset: utf8mb4 //ここを追加 encoding: utf8mb4 //ここがutf8になっていた pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: socket: /tmp/mysql.sock rails db:create にて再度、データベース構築。 あとは一番最初の方法でインポート可能。 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuにデプロイするお作法

本記事について ・Railsのアプリをデプロイする人向けの記事です。(他の言語でも可) ・実際にデプロイするときに詰まったことを含めて書きたいと思います。 前提 ・herokuのアカウントを持っている。 ・herokuをローカルまたはCloud9等にインストールしている。 ・Railsのプロジェクトがある。(完成してなくても良い) ・MySQLを使用している。 手順 1.herokuにログイン $ heroku login 2.作成したRailsプロジェクトのディレクトリに移動し、herokuにアプリケーションを作成 $ heroku apps:create <アプリの名前> ・この時<アプリの名前>は一意の名前するようにしましょう。 ・一意の名前とは世界でたった一つだけの名前のことです。 3.herokuのリモートリポジトリを確認 $ git remote -v ・上記コマンドを実行すると以下のように出力される。   heroku https://git.heroku.com/<アプリの名前>.git (fetch)   heroku https://git.heroku.com/<アプリの名前>.git (push)   origin git@github.com:shou1012/<アプリの名前>.git (fetch)   origin git@github.com:shou1012/<アプリの名前>.git (push) 4.リモートリポジトリherokuが作成されてることを確認 $ git remote heroku origin (heroku、またはheroku originが出ていたらOK) 5.DBをMySQLに変更する ・cleardbのigniteプランにする変更コマンド $ heroku addons:create cleardb:ignite *エラー注意* 以下のようなエラーはHerokuアカウントでクレジットカード情報の登録が必要です。 ▸ Please verify your account to install this add-on plan (please enter a credit card) For more ▸ information, see https://devcenter.heroku.com/categories/billing Verify now at ▸ https://heroku.com/verify 「Igniteプラン」は無料のためこの設定だけで料金が発生することはないです。 ・ClearDBのURL確認 以下のコマンドで、ClearDBのURLが確認できます。 $ heroku config === <アプリの名前> Config Vars CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=tru ・環境変数の設定をする。 上記コマンドで表示されたそれぞれの値を変数に設定する。 $ heroku config:add DB_NAME='<データベース名>’ $ heroku config:add DB_USERNAME='<ユーザー名>’ $ heroku config:add DB_PASSWORD='<パスワード>’ $ heroku config:add DB_HOSTNAME='<ホスト名>’ $ heroku config:add DB_PORT=’3306′ $ heroku config:add DATABASE_URL=’mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true’ ・設定内容を確認する。 $ heroku config === <アプリの名前> Config Vars CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true DATABASE_URL: mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true DB_HOSTNAME: <ホスト名> DB_NAME: <データベース名> DB_PASSWORD: <パスワード>DB_PORT: 3306DB_USERNAME: <ユーザー名> ・Railsのプロジェクトのために準備をする config/environments/production.rb config.assets.compile = true config.assets.initialize_on_precompile=false rootへのルーティング作成はHerokuでアプリケーションにアクセスした時に最初に開くページをRails側で設定してくれという意味。 6.Herokuへデプロイ ・ローカルリポジトリをherokuへpush $ git push heroku master ・herokuのDBのmigrate $ heroku rake db:migrate ・アプリケーションを開く $ heroku open 引用 https://mabeblog.com/heroku-rails-mysql
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails データカラムの変更方法

$ rails g migration change_data_[カラム名]_to_[テーブル名] 実行するとマイグレーションファイルが作成されるので def change change_column :テーブル名, :カラム名, :型 end を記入 rails db:migrate で変更完了。 Sequel Proなどを使用して、視覚的に確認して、変更されていた。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでMaterial Design Iconsを使おう!(もともと使っていたGemが削除されたので新しいのを作ったという話)

とにかくRailsでMaterial Design Iconsつかいたい! という方はインストールと使い方を読んでもらえれば大丈夫です。 Gem作った背景などはそのあとに続きます。 インストール Gemfileに以下を追加します。 gem 'rails_material_design_icons' を追加し、bundle install を実行します。 使い方 application.css`で、cssファイルをインクルードします。 /* *= require materialdesignicons */ おめでとう! 公式サイトで利用可能なアイコンをチェックしましょう。 https://materialdesignicons.com/。 Sass 以下を application.scss ファイルに追加してください。 @import "materialdesignicons"; Gem作った背景 仕事でなんとなくあるMaterial Design IconsのGemを使っていたが、ある日それがRubygemsから削除されました。 使っていたのはこちらのMaterial Design Iconsです。 https://github.com/barrymieny/material_design_icons ですが、Rubygemsをみると以下の記載があります This gem previously existed, but has been removed by its owner. The RubyGems.org team has reserved this gem name for 36 more days. After that time is up, anyone will be able to claim this gem name using gem push. If you are the previous owner of this gem, you can change ownership of this gem using the gem owner command. You can also create new versions of this gem using gem push. Gem自体がもうないです。 他に似たGemもありましたが、最近更新されているものはなかったです。 またinline_svgなどの機能もいらないので、できればない方が嬉しいです。 もともと使っていたGemのコードをみると、テストもなく、これだときつい・・・と思ったのですが、FontAwesomeのRailsインテグレーションのGemだとコードがキレイで、Github Actionsで自動テストもされていました。これをMaterialDesignIcons用に改修しよう!と決めて、一日有給取って、初めてのGem公開しました! (仕事で使えなくなって困っていたので。。。それに有給使ってしまいました。楽しかったです。) またMaterial Design Iconsの新しいバージョンがでたらアップデート作業もありますが、すくなくともSVGはないです。 まえ使っていたGemは実は3200個のSVGファイルを読み込んでいたみたいで、使っていなかったので、もったいないという感じでした。 Gemを入れるときはちゃんとソースまで確認しましょう! という話でした。 ソース自体はこちらです https://github.com/sampokuokkanen/rails_material_design_icons 失敗は成功の元 ちなみに、バージョンを見ると https://rubygems.org/gems/rails_material_design_icons 最新のバージョンだけにフォントファイルなどが入っているのがわかります。 初めてのGemだったので、Gemspecファイルでファイル取り込みで失敗していました。 基本的にはデフォルトで入るコマンドでいいかと思いますが、もともとのGemのコマンドが入っていて、ちゃんとファイルが入らなかったです。 これもGitから直接Railsに取り入れるとちゃんと入っていたので、解決にはちょっと時間かかりました。 今はデフォルトでも入っているコマンドの以下にしています: gem.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【rails6】Action Cableのチャットにユーザーのアイコン画像も送信したい

こんにちは! 今日は「Action Cableを用いて作成したメッセージ機能の不具合解消」のご報告となります。 最後までお付き合いくださいませ 「Action Cableを用いたメッセージ機能を実装したい!!」という方は予め下記をご参考に実装くださいませ 参考文献(メッセージ機能の実装について) https://qiita.com/Takahiro_Ito/items/f27cc9e991fdb9347eee https://techtechmedia.com/action-cable-rails6/ https://qiita.com/take18k_tech/items/00cc14c0eff45073ffc7 解消したかった事 メッセージ送信時にユーザーのアイコン画像が送付できなかった 環境 Ruby on Rails 6.0 AWS/EC2 Nginx MariaDB unicorn エディタはVS Codeを使用しています jQuery導入 既にAction Cableで非同期通信に成功していること まずは今日の完成形を見てみましょう やりたい事 この通り、非同期でメッセージを送信していますが、 メッセージだけでなく「ユーザーのアイコン画像」も表示できていることがわかります。 これを実装しましょう!! javascript/channels/room_channel.js を編集する javascript/channels/room_channel.js received(data) { // アイコン情報を取得します const $userIcon = document.getElementById('js-user-icon'); const $defaultIcon = document.getElementById('js-default-icon'); // 条件分岐でアイコン情報を返却します function icon(){ if( $userIcon === null ){ return $defaultIcon } else { return $userIcon }; } // 〜 中略 〜 const html = `<div class="incoming-msg"> <div class="img-box"> <div class="avatar-img"> // ↓ここに画像配置の記述をしていきます <img src= "${icon()["src"]}" width="50" height="50" class="img-circle"> </div> </div> <div class="partner-info"> <div class="partner-name"> ${data.user_image.record.nickname} </div> <div class="received-msg"> <div class="received_withd_msg"> ${data.message.message} </div> <div class="created-time"> 今 </div> </div> </div> </div>`; } // 見づらくて申し訳ないですが、上記記述は全てreceived(data)の中に記述しているものです では、肝心な画像の部分を抜粋して見てみましょう <img src= "${icon()["src"]}" width="50" height="50" class="img-circle"> icon() => 先ほどの条件分岐を呼び出しています ["src"] => 下記をご確認ください 画像はIcon情報内の「 src 」に含まれていますので、icon()["src"]と記述しましょう viewにid情報を付与して完成です 処理の記述は上記で完了です、viewファイルにidを付与してあげましょう show.html.erb <div class="img-box"> <div class="avatar-img"> <%= link_to user_path(m.user.id) do %> <% if m.user.avatar.attached? %> ↓ 以下が画像の欄です idを付与しましょう ↓ <%= image_tag(rails_blob_path(m.user.avatar), size: "50x50", class: "img-circle", id:"js-user-icon") %> <% else %> <%= image_tag("default_user.png", size: "50x50", class: "img-circle", id:"js-default-icon") %> <% end %> <% end %> </div> </div> これで完成です! 今日もご覧いただきありがとうございました! もし、もっと簡単に書ける方法や処理を軽くする方法などございましたら是非コメント頂けますと幸いです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

外部のHTML,CSS,JavaScriptをRailsで表示させる。

自作のWebアプリケーションを作成するときに、あらかじめHTMLテンプレートでテンプレートを作成した後に、それをRailsのアプリケーションに反映させよう、と言う考えのもと、作成しました。 結論めっちゃ失敗した 簡単に失敗したところ記載します。 単純にassets/stylesheet/に配置 CSSは読み込んでくれました。 ただ中身に注意。 特にimage-urlなどrubyのcssと通常のcssで記法が異なる点があります。 ここまではまだ良かったです。 ネックはJavaScript もうこれは思い出したくないくらいエラーが出ました。 その度に解決方法を模索して、結果として下記を実行したら、解消されました。 ①jQueryの導入 これは割と初期でやりました。 ②複数のjsファイルを全部application.jsに転記 これは全然ダメでした。 ③application.jsの初期で記述されているrequireをコメントアウトして、importで追加のjsファイルを読み込む これが効果抜群でほぼ解決しました。 言ってもまだ全部解決してない まだ解決してないし、なぜ初期で記述されているrequireがダメだったのかは考察が必要ですが、一旦、やりたいことできたので、ほんとにホッとしてます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

外部のHTML,CSS,JavaScriptをRailsで表示させる(Turbolinksが原因っぽい)

自作のWebアプリケーションを作成するときに、あらかじめHTMLテンプレートでテンプレートを作成した後に、それをRailsのアプリケーションに反映させよう、と言う考えのもと、作成しました。 結論めっちゃ失敗した 簡単に失敗したところ記載します。 単純にassets/stylesheet/に配置 CSSは読み込んでくれました。 ただ中身に注意。 特にimage-urlなどrubyのcssと通常のcssで記法が異なる点があります。 ここまではまだ良かったです。 ネックはJavaScript もうこれは思い出したくないくらいエラーが出ました。 その度に解決方法を模索して、結果として下記を実行したら、解消されました。 ①jQueryの導入 これは割と初期でやりました。 ②複数のjsファイルを全部application.jsに転記 これは全然ダメでした。 ③application.jsの初期で記述されているrequireをコメントアウトして、importで追加のjsファイルを読み込む これが効果抜群でほぼ解決しました。 追記:Turbolinksが悪さをしていたっぽい 特にエラーが出ていた時はjQueryが読み込まれていないことが原因でした。 Uncaught TypeError: webpack_require(...) is not a functionが出ていました。 該当の行を確認すると、確かにjQueryが読み込まれていないことが原因。 で、調べてみるとTurbolinksが悪さをしている可能性が。 【Rails】初心者向け!画面遷移の高速化を行うTurbolinksについて図を用いて詳しく解説 Turbolinksは簡単に言うとページの表示を高速化させるためのライブラリです。 いわゆる非同期処理というものです。 しかし一方で、readyやwidowのイベントが発火しない、といったトラブルが発生しているようです。 今回はこれがほぼ可能性が高い。 なぜならapplication.jsに window.jQuery = window.$ = require("./jquery.min") というwindowのイベントを定義していたにもかかわらず、イベントが発火していなかったから。 特にRails4以降は標準で入っているライブラリなので、jsが読み込まない、ということであれば、デフォルトで入っているライブラリを一つずつコメントアウトしてみると良いかもしれません。 (もちろんページの表示速度は遅くなるだろうが。) 言ってもまだ全部解決してない まだ解決してないし、なぜ初期で記述されているrequireがダメだったのかは考察が必要ですが、一旦、やりたいことできたので、ほんとにホッとしてます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでjQueryのfadeOutを使おうと思ったらエラーが起きた

やろうとしたこと RailsのflashメッセージをjQueryでフェードアウトさせたい!と思い、以下の記事を参考にしてjQueryを導入しました。 そして、以下のように実装しました、 <script type="text/javascript"> $(function() { setTimeout($('#フェードアウトさせたい要素').fadeOut('slow'), 2000); }); </script> エラー しかし、上手くフェードアウトせず。 コンソール画面を見てみると以下のようなエラーが出ていました。 このエラーを解決するべくGoogleで調べてみたところ、以下のような記事が出てきました。 この記事によれば、slim版のjQueryを使っているのが良くないとのこと。 しかし、jQueryを導入する際にslim版を指定した覚えもなければ、どうやって変更するのかもわかりませんでした。 そこで、色々と自分の書いたコードを確認していたところ、このviewファイルの一番下に以下のような記述がありました。 index.html.erb #省略 <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> どうやら、過去の自分がCDNのslim版jQueryを読み込んでいたようです。しっかりしろよ…。 この記述を消したところ、無事にフェードアウトさせることができました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] 閲覧数を計測するライブラリ

はじめに 自作のアプリケーションで記事を投稿する機能をつける時に閲覧数を表示させたかったので、以下を参考にimpressionistというGemを使い実装しました。 環境 ruby 2.6.6 rails 6.0.3 やり方 1.impressionistのインストール Gemfile gem 'impressionnist' 上記の記述をしてbundle installをしましょう。 2.impressionnists_tableの作成 以下のコマンドを入力するとimpressionistが定義した、impressionsテーブルをしようすることができます。 rails g impressionist rails db:migrateを実行します。 rails db:migrate すると以下のようなmigrationファイルが作成されました。 class CreateImpressionsTable < ActiveRecord::Migration[6.0] def self.up create_table :impressions, :force => true do |t| t.string :impressionable_type t.integer :impressionable_id t.integer :user_id t.string :controller_name t.string :action_name t.string :view_name t.string :request_hash t.string :ip_address t.string :session_hash t.text :message t.text :referrer t.text :params t.timestamps end add_index :impressions, [:impressionable_type, :message, :impressionable_id], :name => "impressionable_type_message_index", :unique => false, :length => {:message => 255 } add_index :impressions, [:impressionable_type, :impressionable_id, :request_hash], :name => "poly_request_index", :unique => false add_index :impressions, [:impressionable_type, :impressionable_id, :ip_address], :name => "poly_ip_index", :unique => false add_index :impressions, [:impressionable_type, :impressionable_id, :session_hash], :name => "poly_session_index", :unique => false add_index :impressions, [:controller_name,:action_name,:request_hash], :name => "controlleraction_request_index", :unique => false add_index :impressions, [:controller_name,:action_name,:ip_address], :name => "controlleraction_ip_index", :unique => false add_index :impressions, [:controller_name,:action_name,:session_hash], :name => "controlleraction_session_index", :unique => false add_index :impressions, [:impressionable_type, :impressionable_id, :params], :name => "poly_params_request_index", :unique => false, :length => {:params => 255 } add_index :impressions, :user_id end def self.down drop_table :impressions end end 閲覧数を計測したいモデルとコントローラーに記述を追加する。 今回は記事を投稿できるアプリケーションを想定してPostテーブルを使用してimpressionist用の記述をしていきます。 posts_controller.rb class QuestionsController < ApplicationController impressionist :actions=> [:show] def show @post = Post.find(params[:id]) impressionist(@post, nil, unique: [:session_hash]) end end 上記のimpressionistををみると第1引数に閲覧する記事のインスタンス、第2引数にオプション(今回は指定なし)、第3引数のunique: [:session_hash]は同じ人がアクセスした場合と複数回同じ記事をみた場合は1回分のアクセスという設定をしています。 最後に計測したいモデルに以下を追加します。 post.rb class Post < ApplicationRecord is_impressionable end ブラウザで閲覧数を確認する場合以下の記述をします。 show.html.erb <%= @post.impressionist_count %> 以上で完了です。 おまけ 閲覧数を表示するのに使いやすいものがFontAwesomeにあったので追記しておきます。 show.html.erb <i class="far fa-eye"></i><%= @post.impressionist_count %> こんな感じになります! 気に入ったら使ってみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HerokuへのPush時に「Failed to install gems via Bundler.」が出た時の対処法

RailsチュートリアルでherokuへPushする際以下のようなエラーが出た。 remote: -----> Ruby app detected remote: -----> Installing bundler 2.2.16 remote: -----> Removing BUNDLED WITH version in the Gemfile.lock remote: -----> Compiling Ruby/Rails remote: -----> Using Ruby version: ruby-2.6.6 remote: -----> Installing dependencies using bundler 2.2.16 remote: Running: BUNDLE_WITHOUT='development:test' BUNDLE_PATH=vendor/bundle BUNDLE_BIN=vendor/bundle/bin BUNDLE_DEPLOYMENT=1 bundle install -j4 remote: Your bundle only supports platforms ["x86_64-darwin-20"] but your local platform remote: is x86_64-linux. Add the current platform to the lockfile with `bundle lock remote: --add-platform x86_64-linux` and try again. remote: Bundler Output: Your bundle only supports platforms ["x86_64-darwin-20"] but your local platform remote: is x86_64-linux. Add the current platform to the lockfile with `bundle lock remote: --add-platform x86_64-linux` and try again. remote: remote: ! remote: ! Failed to install gems via Bundler. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed remote: ! 調べたところ、Heroku環境のbundlerのversionとローカル環境のbundlerのversionが一致していないことが原因らしい。 Herokuのbundlerがバージョン2.2.16なのに対してローカルのバージョンは2.2.19だった。 解決方法 下記のコマンドを順に入力したらうまくいった。 $ gem uninstall bundler # ローカルのbundlerをアンインストール $ gem install bundler --version '2.2.16' # Herokuと同じバージョンのbundlerをインストール $ bundler -v # 正しくインストールされたか確認 $ rm Gemfile.lock # 今あるGemfile.lockを削除する $ bundle _2.2.16_ install --without production # bundleをインストール & Gemfile.lockが作成される $ bundle lock --add-platform x86_64-linux $ git add -A $ git commit -m "Change bundler version 2.2.19->2.2.16" $ git push heroku master エラー文でググったら似たような事例がたくさん出てきた。 追記 remote: ! Unknown version "6.1" このエラーは、Gemfileを書き換えるとうまくいった。 Gemfile gem 'rails', '6.0.3' こうする↓ Gemfile gem 'rails', '6.1.0'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails ログインユーザーが、URLを入力しても特定のページに遷移できないようにする

作成していた投稿アプリで 「ログインしているユーザー以外が投稿した記事の編集ページに飛ぼうとすると、トップページに遷移する」 という機能がありました。 Aさんが投稿したid=1という記事の編集(edit)にBさんがアクセスできないようにする、ということですね。 実装しなきゃいけないのは、単純にeditメソッドだけでなく、ログインユーザー以外がeditページにアクセスした時、という条件を付けなければいけないということ。 def edit @prototype = Prototype.find(params[:id]) unless current_user.id == @prototype.user_id redirect_to root_path end end 解説します。 今回でいう記事はprototypeですね。 editアクションが実行された時、prototypeから該当のidをfindで見つけて、@prototypeに格納します。 で、unlessで unless current_user.id == @prototype.user_id redirect_to root_path end という条件をつけました。 deviseを使用しているので、current_userとなりますが、要はログインユーザーと@prototypeのユーザーIDが一致しない時はリダイレクトでroot_pathに戻されます。 参考文献 ログインユーザーが、URLを入力しても特定のページに遷移できないようにする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 投稿、入力フォームで空欄があったときにデータを保持した状態にする

作成した投稿アプリケーションの仕様上、 「空欄で投稿したときに、値は保持したままで、入力フォームに戻ってくる」 という仕様がありました。 これの再現方法について def create @prototype = Prototype.new(prototype_params) if @prototype.save redirect_to root_path else render action: :new end end いきなり結論、これでいけました。 順に解説 @prototype = Prototype.new(prototype_params) ここで@prototypeというインスタンス変数に.newメソッドで生成した値を入れておきます。 if @prototype.save redirect_to root_path else render action: :new これで.saveメソッドが実行された時はroot pathに戻されますが、そうではない時はrenderアクションでnewメソッドに戻ります。 つまり大事なのが、redirectとrenderの違い。 renderはインスタンス変数の値はそのままです。 そう、newメソッドに戻った時の@prototypeの値を引き継ぎます。 逆に、redirectは値を引き継がず、ページ遷移するだけです。 基本的な動作ですが、画面遷移を伴う場合、インスタンス変数が現在どの値になっているのかは正確に把握する必要がありますね。 参考文献 フォーム入力時のバリデーション機能(値のチェック)を実装する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Headless CMSのContentfulをRailsで使ってみる~実装編1~

本記事の趣旨 Contentfulについて別記事に書きましたが、 なかなか使用するにはハードルを感じたので、私が作成した共通処理を一例として公開したいと思います。 まだまだ荒いところもあるかと思うので、あくまで参考まで。 前の記事 https://qiita.com/polar_bear_tech/items/fac1fbc345662c396e5b Contentful Gem(読み込み用)の難しいところ 以下のポイントを解決するようモデルと共通処理を作っていきます。 Contentful::Entryモデルを継承してinitializeでフィールドを設定する必要がある エントリー取得時のクエリが長く、感覚的に使いにくい モデル間のリレーションがあると深いJSONになり扱いにくい 基本的な実装 Contentfulで作成したモデルには"contentful_readable.rb"をincludeすることにしています。 ここにclientを実装する時、entry_mappingにContentfulGemで扱うモデルをマッピングします。 app/models/concerns/contentful_readable.rb require 'contentful' module ContentfulReadable extend ActiveSupport::Concern included do class << self def cda_client @cda_client ||= Contentful::Client.new( space: ENV['CONTENTFUL_SPACE_ID'], access_token: ENV['CONTENTFUL_DELIVERY_ACCESS_TOKEN'], environment: ENV['CONTENTFUL_ENVIRONMENT'] || 'master', # ↓ここの数値が多いとリレーション項目を持ってきてしまってクエリが重くなる max_include_resolution_depth: 1, timeout_read: 30, timeout_write: 30, timeout_connect: 10, # Contentfulのモデルをマッピングする。左辺がContentModelのID、右辺がモデルクラス entry_mapping: { 'user' => User } ) end end end end ContentfulGemで扱われるモデルには、Contentful::Entryを継承させます。 忘れず先程の共通処理をincludeさせます。 id,first_name,usersとタイムスタンプのフィールドを持つモデルとして、アクセサを定義した時、 initializeではそれぞれに初期化時に値を入れるよう処理を記載する必要があります。 app/models/user.rb class User < Contentful::Entry include ContentfulReadable attr_accessor :id, :first_name, :user_ids, # ユーザー同士の多対多リレーション項目usersのIDを配列で持つ :created_at, :updated_at def initialize( item = {}, _configuration = {}, localized = false, includes = [], entries = {}, depth = 0, errors = [] ) # Contentfulがデフォルトで持っている項目はsysから self.id = item.dig('sys', 'id') self.created_at = item.dig('sys', 'createdAt') self.updated_at = item.dig('sys', 'updatedAt') # Content Model定義時に設定したフィールドはfieldsから self.first_name = item.dig('fields', 'firstName') # リレーション項目はさらにネストされたsysから self.user_ids = item.dig('fields', 'users').map { |hash| dig('sys', 'id') } # 注) 上記は必ず値が入っている場合の例 # Contentful::Entryのinitializeも動くようにsuperを実行 super end end このモデルのクエリを発行するとき、以下のようになります。 User.cda_client.entries( content_type: 'user', 'fields.firstName[eq]' => 'Tarou', 'fields.users.sys.contentType.sys.id' => 'user', 'fields.users.fields.firstName[eq]' => 'Jun' ) [eq]というのは一致条件で、他にも[ne][in][lte][gte]などがあります。(詳しくはCDAのリファレンスを参照) 条件が多かったり、リレーション項目で条件をつける場合、例のように長くなります。 共通処理を充実させて簡単にしてみる 結論から、共通処理の最終形です。 app/models/concerns/contentful_readable.rb # frozen_string_literal: true require 'contentful' module ContentfulReadable extend ActiveSupport::Concern included do class << self def relation_field(field) @relations = [] if @relations.nil? @relations.push(field) end attr_reader :relations # すべてのユーザーを取得する def all cnt_array = cda_client.entries(content_type: name.camelize(:lower)) items = cnt_array.items.dup Enumerator::Lazy.new(0...cnt_array.total) do |yielder, _index| yielder << items.shift if items.empty? cnt_array = cnt_array.next_page(cda_client) items = cnt_array.items.dup end end end def total cda_client.entries(content_type: name.camelize(:lower), limit: 0).total end alias_method :count, :total # Hash型式で条件を渡すと一致するエントリを配列で返す # conditions<Hash>: # keyに検索対象のフィールド名と演算子名をsnakeケースで設定し、検索値をvalueに設定する。 # 演算子名はcontentfulのAPIドキュメントに準拠する # https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters # 例: { field_name_eq: value, field_name_gte: value } # order<Hash>: # keyに順序の指定項目を、valueにasc/descを指定する。 # 複数条件を指定した場合、先の要素が優先される。(例の場合はupdated_atの降順の後にidで昇順となる) # 例: { updated_at: "desc", id: "asc" } # include_level<Integer>: 複数のエントリーを取得する場合に、関係を持つエントリーを併せて取得する # limit<Integer>: 取得エントリー数の上限(default: 100) # skip<Integer>: クエリ総数がlimitを超える場合に、スキップするページ数を指定する def where(conditions, order: { updated_at: 'desc', id: 'asc' }, include_level: 0, limit: 100, skip: 0) query_params = { content_type: name.camelize(:lower), include: include_level, order: order_format(order), limit: limit, skip: skip } conditions_format(conditions).each { |k, v| query_params.store(k, v) } cda_client.entries(query_params).to_a end def find_by(conditions, include_level: 0, order: { updated_at: 'desc', id: 'asc' }) query_params = { content_type: name.camelize(:lower), include: include_level, order: order_format(order), limit: 1 } conditions_format(conditions).each { |k, v| query_params.store(k, v) } cda_client.entries(query_params).first end # ID指定で1件取得して返す def find(id) cda_client.entry(id) end def cda_client @cda_client ||= Contentful::Client.new( space: ENV['CONTENTFUL_SPACE_ID'], access_token: ENV['CONTENTFUL_DELIVERY_ACCESS_TOKEN'], environment: ENV['CONTENTFUL_ENVIRONMENT'] || 'master', max_include_resolution_depth: 1, timeout_read: 30, timeout_write: 30, timeout_connect: 10, entry_mapping: { 'user' => User } ) end private # 順序指定条件のHashをContebntfulに渡せる形式に変換する # in: { updated_at: desc, id: asc } # out: "-sys.updatedAt,sys.id" def order_format(order_hash) formatted_orders = [] order_hash.each do |field, order| camelized_field = field.to_s.camelize(:lower) prefix = %w[id createdAt updatedAt].include?(camelized_field) ? 'sys' : 'fields' order_symbol = order.casecmp('desc').zero? ? '-' : '' formatted_orders.push("#{order_symbol}#{prefix}.#{camelized_field}") end formatted_orders.join(',') end # 検索条件のHashをContebntfulに渡せる形式に変換する def conditions_format(conditions) formatted_conditions = {} conditions.each do |field_cnd, value| if value.is_a?(Hash) conditions_format(value).each do |link_condition, link_value| camelized_field = field_cnd.to_s.camelize(:lower) formatted_conditions.store("fields.#{camelized_field}.sys.contentType.sys.id", camelized_field) formatted_conditions.store("fields.#{camelized_field}.#{link_condition}", link_value) end elsif field_cnd.to_s == 'query' formatted_conditions.store(:query, value) else field_cnd_array = field_cnd.to_s.split('_') cnd = field_cnd_array.pop cnd = cnd == 'eq' ? '' : "[#{cnd}]" if cnd == '' && value.nil? cnd = '[exists]' value = false end field = field_cnd_array.join('_') camelized_field = field.to_s.camelize(:lower) prefix = %w[id createdAt updatedAt].include?(camelized_field) ? 'sys' : 'fields' formatted_conditions.store("#{prefix}.#{camelized_field}#{cnd}", value) end end formatted_conditions end end end private def initialize_field_set(item) return if item.blank? self.id = item.dig('sys', 'id') self.created_at = item.dig('sys', 'createdAt') self.updated_at = item.dig('sys', 'updatedAt') item['fields'].each do |key, value| if self.class.relations.present? && self.class.relations.include?(key.underscore) case value when Hash send("#{key.underscore}_id=", value.dig('sys', 'id')) if self.class.method_defined?("#{key.underscore}_id=") when Array if self.class.method_defined?("#{key.underscore.singularize}_ids=") send("#{key.underscore.singularize}_ids=", value.map { |hash| hash.dig('sys', 'id') }) end end elsif self.class.method_defined?("#{key.underscore}=") send("#{key.underscore}=", value) end end end end 併せてモデルの最終形。 app/models/user.rb class User < Contentful::Entry include ContentfulReadable attr_accessor :id, :first_name, :user_ids, # ユーザー同士の多対多リレーション項目usersのIDを配列で持つ :created_at, :updated_at relation_field 'users' def initialize( item = {}, _configuration = {}, localized = false, includes = [], entries = {}, depth = 0, errors = [] ) # 共通処理 initialize_field_set(item) # Contentful::Entryのinitializeも動くようにsuperを実行 super end # usersでリレーションを持つ、Userモデルを取得する def friends return [] if user_ids.blank? @users ||= User.where( { id_in: user_ids }, order: { updated_at: 'desc'} ) end end 上記までの最終形では、 Contentfulのフィールド名とRails上のフィールド名に一定のルールを設け、 それに則った状態で使用できるようにしています。たとえば下記のような感じ。 項目 Contentful Rails 非リレーション項目 firstName first_name 複数リレーション項目 friendUsers friend_user_ids 単数リレーション項目 friendUser friend_user_id 軽くそれぞれのポイントを触れていきます。 リレーション項目の表現 共通処理から抜粋。 ここでリレーション項目をモデルに定義できるようにしています。 定義されたリレーション項目はinitialize、クエリの共通処理で使用しています。 def relation_field(field) @relations = [] if @relations.nil? @relations.push(field) end attr_reader :relations initializeの簡略化 共通処理から抜粋。 コメントの通り、命名ルールに則って初期値を入れていきます。 def initialize_field_set(item) return if item.blank? # Contentfulのデフォルト項目はそのまま入れる self.id = item.dig('sys', 'id') self.created_at = item.dig('sys', 'createdAt') self.updated_at = item.dig('sys', 'updatedAt') # ContentModelに定義した項目は・・・ item['fields'].each do |key, value| # リレーション項目なら if self.class.relations.present? && self.class.relations.include?(key.underscore) case value # 単数リレーションなら when Hash # IDを所定の名前の項目に入れる send("#{key.underscore}_id=", value.dig('sys', 'id')) if self.class.method_defined?("#{key.underscore}_id=") # 複数リレーションなら when Array if self.class.method_defined?("#{key.underscore.singularize}_ids=") # IDの配列を所定の名前の項目に入れる send("#{key.underscore.singularize}_ids=", value.map { |hash| hash.dig('sys', 'id') }) end end # 非リレーション項目なら elsif self.class.method_defined?("#{key.underscore}=") # 値を所定の名前の項目に入れる send("#{key.underscore}=", value) end end end クエリの簡略化 共通処理から抜粋。 コメント記載のルールで条件を指定して、クエリを実行します。 # Hash型式で条件を渡すと一致するエントリを配列で返す # conditions<Hash>: # keyに検索対象のフィールド名と演算子名をsnakeケースで設定し、検索値をvalueに設定する。 # 演算子名はcontentfulのAPIドキュメントに準拠する # https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters # 例: { field_name_eq: value, field_name_gte: value } # order<Hash>: # keyに順序の指定項目を、valueにasc/descを指定する。 # 複数条件を指定した場合、先の要素が優先される。(例の場合はupdated_atの降順の後にidで昇順となる) # 例: { updated_at: "desc", id: "asc" } # include_level<Integer>: 複数のエントリーを取得する場合に、関係を持つエントリーを併せて取得する # limit<Integer>: 取得エントリー数の上限(default: 100) # skip<Integer>: クエリ総数がlimitを超える場合に、スキップするページ数を指定する def where(conditions, order: { updated_at: 'desc', id: 'asc' }, include_level: 0, limit: 100, skip: 0) query_params = { content_type: name.camelize(:lower), include: include_level, order: order_format(order), limit: limit, skip: skip } # 条件指定をContentful形式に整えてパラメータにマージする conditions_format(conditions).each { |k, v| query_params.store(k, v) } # エントリーを取得 cda_client.entries(query_params).to_a end # 順序指定条件のHashをContebntfulに渡せる形式に変換する # in: { updated_at: desc, id: asc } # out: "-sys.updatedAt,sys.id" def order_format(order_hash) formatted_orders = [] order_hash.each do |field, order| # キャメルケースに変換(created_at => createdAt) camelized_field = field.to_s.camelize(:lower) # sysかfieldsを判断 prefix = %w[id createdAt updatedAt].include?(camelized_field) ? 'sys' : 'fields' # desc指定の場合は-をつける order_symbol = order.casecmp('desc').zero? ? '-' : '' formatted_orders.push("#{order_symbol}#{prefix}.#{camelized_field}") end # 条件をカンマ区切り文字列にして返す formatted_orders.join(',') end # 検索条件のHashをContebntfulに渡せる形式に変換する def conditions_format(conditions) formatted_conditions = {} # 下記のクエリを発行した場合(ユーザー名Taroで、フレンドにJunがいるユーザー) # User.where({ # first_name_eq: 'Taro', # users: { first_name_eq: 'Jun' } # }) conditions.each do |field_cnd, value| # リレーション項目の条件指定の場合 if value.is_a?(Hash) # 指定された{ first_name_eq: 'Jun' }を再起的に取得して # { 'fields.users.firstName[eq]' => 'Jun' }をEachで回す conditions_format(value).each do |link_condition, link_value| camelized_field = field_cnd.to_s.camelize(:lower) # リレーション項目のID指定条件を追加 { "fields.users.sys.contentType.sys.id" => 'user' } formatted_conditions.store("fields.#{camelized_field}.sys.contentType.sys.id", camelized_field) # リレーション項目のfirstName条件を追加 { "fields.users.fields.firstName[eq]" => 'Jun' } formatted_conditions.store("fields.#{camelized_field}.#{link_condition}", link_value) end # 'query'が指定されていた場合、条件をそのまま入れる elsif field_cnd.to_s == 'query' formatted_conditions.store(:query, value) # 非リレーション項目の条件指定の場合 else # first_name_eq => firstName[eq]にする field_cnd_array = field_cnd.to_s.split('_') cnd = field_cnd_array.pop cnd = cnd == 'eq' ? '' : "[#{cnd}]" if cnd == '' && value.nil? cnd = '[exists]' value = false end field = field_cnd_array.join('_') camelized_field = field.to_s.camelize(:lower) # Contentfulのデフォルト項目はsys,それ以外はfieldsをつける prefix = %w[id createdAt updatedAt].include?(camelized_field) ? 'sys' : 'fields' # 条件を追加 { 'fields.firstName[eq]' => 'Taro' } formatted_conditions.store("#{prefix}.#{camelized_field}#{cnd}", value) end end # 条件のHashを返す # { # 'fields.firstName[eq]' => 'Taro', # 'fields.users.sys.contentType.sys.id' => 'user', # 'fields.users.fields.firstName[eq]' => 'Jun' # } formatted_conditions end 例えば、ユーザーモデルから抜粋して、こんなかんじに。 CDAのクエリ用にeq,ne,gthとかの条件指定文字列の名残はありますが、 冗長だったfields.sys...うんぬんはリレーションを見て勝手に補完するようにしてすっきりしました。 ActiveRecordちっくに扱えるのでマシになったかと個人的に思っています。 # usersのUserモデルを取得する def friends return [] if user_ids.blank? @users ||= User.where( { id_in: user_ids }, order: { updated_at: 'desc'} ) end あとがき サンプル実装がコピーしてそのまま動かなかったらすみません、元実装を編集したので・・・ 大丈夫だと思いますが、あくまで参考にしてください。 あまり私が実装していた時点では参考記事が少なかったので、一助になればと思います。 共通処理をここまで充実させれば、割と不自由なくContentfulが扱えますが、 「OR条件検索ができない」とかそういう限界は乗り越えられないのでご注意くださいね。 書き込み用Gem ContentfulManagementについては、いずれ・・・いずれ書くかもしれません。 需要があればその旨コメントいただけるとモチベーションになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuデプロイ後に開発環境でミスがあったときの対処方法

Herokuにデプロイした後はアプリケーションのエラーは このように表示されるので、何が起きているのかわからない ということで指示にある通り、logをみましょう。 ターミナルで heroku logs --tail --app アプリケーション名 アプリ名がわからない場合は heroku info でわかります。 で、ログを見ると NoMethodError (undefined method `id' for nil:NilClass): ありました。 このエラーの原因はほんとクソみたいな原因なので、解消方法は割愛。 要するに、開発環境の時点でミスっていたわけです。 コードを修正した後は再度、Gitでプッシュして git push heroku master を実行。 要するに開発環境だけ修正してもHerokuにデプロイしたものが修正されるわけではないので、再度Herokuへのプッシュが必要だよということです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] マイグレーションファイルの削除方法

マイグレーションファイルの状態確認 ファイルのステータスがupだと適用されていて、downだと適用されていない状態。 upのままファイルを削除すると、ファイルは存在しないのに適用されているという変な状況になる。 そのため、ターミナル等で下記コマンドを実行し、状態を確認する。 rails db:migrate:status ステータスの変更 ステータスがdownになれば削除可能になるので、下記コマンドで変更する。 rails db:migrate:down VERSION=ファイル名.rb ファイルの削除 あとは消すだけなので、下記コマンドでファイルを削除。 rm -rf パス/ファイル名.rb 終わりに 自分用のメモとして、また、Qiita投稿の練習として書きました。 もし誰かの役に立てば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

slimでlink_toを別タブで表示する

Railsでslimを用いてlink_toを書いた時、target="_blank"で別タブ表示させるのに少し詰まったのでメモ。 例 index.html.slim = link_to "ツイート一覧", tweets_path, target: "_blank" target: "_blank"の位置が重要でした。 余談ですがこちらの記事でも言われてる通り、target="_blank"はセキュリティ的に気をつけたほうがいいらしいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

heroku run db:migrateをしたらエラーが出た

heroku run db:migrateをしたらエラーが出た Mysql2::Error::ConnectionError: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) エラー文を見ると、ローカルのMySQLに接続できませんでした、という感じみたい。 実際にMySQLの設定を確認する。 ターミナルで heroku config を実行 CLEARDB_DATABASE_URL: mysql://******@us-cdbr-east-04.cleardb.com/heroku_********?reconnect=true DATABASE_URL: mysql2 LANG: en_US.UTF-8 RACK_ENV: production RAILS_ENV: production RAILS_LOG_TO_STDOUT: enabled RAILS_MASTER_KEY: **************** RAILS_SERVE_STATIC_FILES: enabled SECRET_KEY_BASE:************* で今回問題になっているのが、 DATABASE_URL: mysql2 ここが本来であれば、URLのようなものが記述されているはずだが、記述されていない。 ということで heroku_cleardb=`heroku config:get CLEARDB_DATABASE_URL` heroku config:set DATABASE_URL=mysql2${heroku_cleardb:5} これをそれぞれ、ターミナルで実行。 再度、heroku configで確認すると CLEARDB_DATABASE_URL: mysql://******@us-cdbr-east-04.cleardb.com/heroku_********?reconnect=true DATABASE_URL: mysql2://******@us-cdbr-east-04.cleardb.com/heroku_********?reconnect=true LANG: en_US.UTF-8 RACK_ENV: production RAILS_ENV: production RAILS_LOG_TO_STDOUT: enabled RAILS_MASTER_KEY: **************** RAILS_SERVE_STATIC_FILES: enabled SECRET_KEY_BASE:************* となっており、この状態で heroku run db:migrate を実行、で解決。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

heroku login --interactiveでHerokuにログインできないとき。

タイトルのコードでログインしようとすると Error: Your account has MFA enabled; API requests using basic authentication with email and password are not supported. Please generate an authorization token for › API access. お前のアカウントはMFAを使ってるからメールアドレスとパスワードだけじゃログイン出来んよ 認証アプリ使ってね ってことみたいですね。 確かに私はそのようにログインしています。 なので heroku login とすると、 heroku: Press any key to open up the browser to login or q to exit: とブラウザ経由でログインができるようになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

link_toメソッドを使用して各記事へのリンクを作成する。

link toメソッドを使用して、パスを指定する際の方法。 ①rails routesでパスを確認する Verb URI Pattern Controller#Action GET /prototypes/:id(.:format) prototypes#show 省略していますが、今回はprototypeという記事の詳細ページへのリンクです。 記事はviewである分だけ表示するよう指定しています。 pathはPrefixで記述した方が良いらしいのですが、今回のprototypes#showにはPrefixがありませんので、URI Patternで記述します。 ②実際の記述 <%= link_to "#{prototype.title}", "/prototypes/#{prototype.id}", class: :card__title%> "/prototypes/#{prototype.id}"の部分がパスですね。 実際のURLもこのようになっています。 http://localhost:3000/prototypes/2  #{}で記述することで、取得した情報を文字列で表示してくれる。 ※URLは文字列型 簡単ですが、link toメソッドを使用して各記事へのリンクの作成方法でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails 指定してルートを確認

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

【Rails】ポリモーフィック関連付けを理解する

エンジニアになりweb開発に今月から携わるようになって初めてポリモーフィック関連付け、というのをやった。ちょっと最初理解に苦しんだのでアウトプットしておく。 ポリモーフィック関連付けとは? 一言でいうと、「一つのアソシエーションで複数のモデルと結びつく」ということ。ER図を用いて説明する。 まず、通常のアソシエーションでは1対1だろうが1対多だろうが一本の矢印が向かう先は1つである。 しかしポリモーフィック関連付けでは一本の矢印から複数のテーブルにアソシエーションが組まれる。 実装方法 ポリモーフィックの実装方法はそんなに難しくない。Railsガイドにも書いてある。 https://bit.ly/3v2wXOX ①マイグレーション実行時にpolymorphic: trueを付与 以下のようなマイグレーションファイルを作る class CreateOrders < ActiveRecord::Migration[5.2] def change create_table :orders do |t| t.date :order_date t.references :orderable, polymorphic: true t.timestamps end end end するとテーブルにimageable_id、imageable_type、という2つのカラムができる。 orderable_id→外部キーにあたるカラム。ただし foreign_key: true はつけない。 orderable_type→どのテーブルと紐付いているのか、の情報を持つ。アソシエーションを組んだモデルのクラス名が入る。 なお、手動で両カラムを設定する方法もある。 class CreateOrders < ActiveRecord::Migration[5.2] def change create_table :orders do |t| t.date :order_date t.bigint :orderable_id t.string :orderable_type t.timestamps end add_index :orders, [:orderable_type, :orderable_id] end end マイグレーションファイルを設定したら rails db:migrate を実行しましょう。 ②アソシエーションを組む ポリモーフィックの場合、「~able」というのがそのままアソシエーション名になる(今回で言えばorderable) class Order < ApplicationRecord belongs_to :orderable, polymorphic: true end class Customer < ApplicationRecord has_many :orders, as: :orderable end class Organization < ApplicationRecord has_many :orders, as: :orderable end どんなメリットがあるの? 無駄な外部キーカラムができなくて済む 今回の例で言えば個人の顧客(Customer)、法人顧客(Organization)のどちらからでも購入されたらordersテーブルにデータを保存するようにしたい場合などに使える。通常個人の顧客が法人も兼ねる、というケースはまれなのでcustomer_id、organization_idと複数のカラムを作ってしまうと紐付いていないテーブルの外部キーがNULLになってしまう。これを避けることができる。 またCustomerとOrganizationで同じメソッドを定義しておけば呼び出しもラクになる。 class Order < ApplicationRecord belongs_to :orderable, polymorphic: true end class Customer < ApplicationRecord has_many :orders, as: :orderable def get_name customer_name end end class Organization < ApplicationRecord has_many :orders, as: :orderable def get_name organization_name end end こんなメソッドは通常定義しないけど、同じ名前のget_nameっていうメソッドを用意しておけば order = Order.new order.orderable.get_name でCustomer, Organizationどちらのメソッドでも呼び出せる。便利。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【統合テスト】Capybaraでテストを書けるようになろう!

Capybaraとは WebブラウザとWebアプリケーションの間で交わされるHTTP通信をエミュレート(模倣)するためのライブラリのことです。 特定の要素をクリックした利、フォームから値を入力したり、表示状態を確認したりできる。 フューチャスペック(統合テスト)に最適です。 事前準備 1.Gemfileに以下を記載し、bundle installする。 ※Rails5.1以降ではデフォルトで入っています。 group :test, :development do gem 'capybara' end 2.spec/rails_helper.rbに以下を記載する。 spec/rails_helper.rb ... require 'capybara/rspec' ... 3,作業フォルダを作成する。 ※spec/features配下にファイルを作成して作業していきます。 $ mkdir -p spec/features 使い方 まずは、例としてcreate_user_spec.rbを書いてみます。 Capybaraの知識0でも、なんとなくやっていることは理解出来るはずです。 まず、シナリオは3つのセクションに分けることがRailsコミュニティの一般的な慣習なので、 Givenセクション、Whenセクション、Thenセクションの3つに分類します。 spec/features/create_user_spec.rb require 'rails_helper' feature 'user', type: :feature do scenario 'ユーザー作成' do # Givenセクション(前提条件) visit new_user_path click_lick 'ユーザーを作成する' first("div.links").click_link "新規登録" # Whenセクション(ブラウザ上の動作) fill_in "form_customer_family_name", with: "テスト" fill_in "form_customer_given_name", with: "太郎" fill_in "form_customer_family_name_kana", with: "テスト" fill_in "form_customer_given_name_kana", with: "タロウ" fill_in "メールアドレス", with: "test@example.jp" fill_in "パスワード", with: "password" fill_in "生年月日", with: "2021-06-08" choose "男性" check "管理者として登録する" click_button "登録" # Thenセクション(シナリオの結果確認) new_user = User.order(:id).last expect(new_user.family_name).to eq("テスト") expect(new_user.given_name).to eq("太郎") expect(new_user.email).to eq("test@example.jp") expect(new_user.birthday).to eq(Date.new(2021. 6. 8)) expect(new_user.gender).to eq("man") end end メソッドの解説 visitメソッド visit 遷移したいページのパスとすることで、指定したページに遷移する。 click_linkメソッド click_link 'リンク文字列 or aタグのid属性'とすることで、aタグをクリックする。 fill_inメソッド fill_in 'ラベル文字列 or input要素のid属性もしくはname属性', with: '入力内容'とすることで、指定のフォームに値を入力できる。 chooseメソッド choose 'ラベル文字列 or ラジオボタンのid属性'とすることで、ラジオボタンを選択出来る。 checkメソッド check 'ラベル文字列 or input要素のid属性もしくはname属性'とすることで、チェックボックスにチェックを入れることができる。 click_buttonメソッド click_button 'ラベル文字列 or input要素のid属性もしくはname属性'とすることで、ボタンを押下できる。 まとめ 直感的にテストを書くことが出来るし、単体テストで検証しきれない部分までテスト可能なので、使えるようにしておきたい技術だと思いました。 より高度で複雑な統合テストを書く際は、参考に添付してある公式ドキュメントやチートシートを参照しながら進めていくといいと思います。 参考 teamcapybara / capybara Capybaraチートシート
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】フォローしたユーザー及びフォロワー一覧表示実装

目的 Railsでフォロー機能を導入したアプリに、フォローしたユーザー及びフォロワー一覧を表示する、 開発環境 macOS: Big Sur Rubyバージョン: 2.6.5 Railsバージョン: 6.0.0 前提 フォロー機能が作成されている。 【Rails】フォロー機能の実装(非同期) 手順 はじめに ルーティングの設定 アクションの定義 一覧ページの作成 はじめに 今回は一覧表示させるために、 ユーザー詳細ページから、フォローしたユーザー及びフォロワー一覧ページへ飛ぶようにします。 ルーティングの設定 まずはルーティングの設定です! config/routes #省略 resources :users do member do get :followings, :followers end end idが必要なため、memberを使用しています。 アクションの定義 アクションの定義です。 usersコントローラーに新たに設定していきます。 app/controllers/users_controller.rb class UsersController < ApplicationController before_action :set_user, only: [:followings, :followers] #省略 def followings @users = @user.followings end def followers @users = @user.followers end private def set_user @user = User.find(params[:id]) end end 一覧ページの作成 ユーザー詳細ページにリンクボタンを設置し、一覧表示をさせます。 app/views/users/show.html.erb <%= link_to "フォロー", followings_user_path(@user) %> <%= link_to "フォロワー", followers_user_path(@user) %> app/views/users/followers.html.erb <p>フォロワー一覧</p><br/> <% @users.each do |user| %> <%= user.name %> <% end %> app/views/users/followings.html.erb <p>フォロー一覧</p><br/> <% @users.each do |user| %> <%= user.name %> <% end %> 最後に 以上で、一覧表示の実装は完了です。 CSSは実装していないため、簡素な見た目となっています。 必要に応じて各自編集をお願いします。 では。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 条件つきバリデーション

条件つきバリデーションとは? 標準であるバリデーションだけでは対応できずに、特定の条件を満たす場合に実行したいバリデーションのこと。 [if],[unless]オプションを使用して記述することで、 特定の条件に対応することができる。 引数にProc,Arrayを使える。 if #特定の条件でバリデーションを行いたい時に使用 unless #特定の条件でバリデーションを行いたくない時に使用 if,unless class Pass < ApplicationRecord validates :number, presence: true, if: :pass_number? def pass_number? pass_type == "pass" end end class Money < ApplicationRecord #money_validate関数の呼び出し validate :money_validate    def money_validate unless money.nil? || money > 0.0 end end Proc 呼び出したいProcオブジェクトを:ifや:unlessで使うこともできます。Procオブジェクトを使うと、個別のメソッドを指定する代りに、その場で条件を書けるようになります。ワンライナーに収まる条件を使いたい場合に最適です ~Railsガイド~ class Account < ApplicationRecord validates :password, confirmation: true, unless: Proc.new { |a| a.password.blank? } end ~Railsガイド~ リファレンスには上記のように記述されていましたが、 Proc。newでオブジェクトを作成、引数aに渡し、 a.password.blank?で真偽値の判断? 条件付きバリデーションのグループ化 with_options 1つの条件を複数のバリデーションでまとめて処理を行うにはwith_optionsを使用 class User < ApplicationRecord with_options if: :is_admin? do |admin| admin.validates :password, length: { minimum: 10 } admin.validates :email, presence: true end end Rails ガイド if: :is_admin?という条件に対してバリデーションが働く Array,配列 バリデーションを行なう条件を複数定義する際には、Array 配列を使用 class Computer < ApplicationRecord validates :mouse, presence: true, if: [Proc.new { |c| c.market.retail? }, :desktop?], unless: Proc.new { |c| c.trackpad.present? } end Railsガイド 備忘録です。 参考サイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む