20210419のRailsに関する記事は18件です。

【rails】Ratyを使った星評価(評価の保存・表示・再評価)

こんばんは。プログラミング初学者です。 railsにてバイクのレビューを投稿するアプリを作成中です。 RatyというjQueryプラグインを用いて星評価を実装していたのですが、導入から表示までは参考記事がそこそこあったのですが、再評価というところが参考記事が少なかったため、本記事にまとめてみました。 間違いがございましたら、遠慮なくご指摘いただけますと幸いです。 前提条件 ・jQuery導入済み ・Raty導入済み ・星評価の対象となるカラム型はfloat型である。(星半分の評価も可能とするため) ※Raty導入につきましては下記の記事が参考になりました。 ・Railsで「Raty」を使った星機能をつける ・[Rails 6] Raty.jsを使った星型レビュー ・【Rails+jQuery Raty】レビュー用の星★の評価を実装する(入力、保存、表示) ・公式GitHub 評価の保存 ビューファイルの書き方 ※form_withを使用しています <%= form_with(model: @review, local: true) do |f| %> <div class="star-form-group" id="star1"> <%#id要素の付与がポイント%> <%= f.label :comfort,'乗り心地', class:'star-title' %> <%#保存したいカラムの指定%> </div> <%= end %> ratyを用いた星評価保存の関数定義 ※同じビューファイルにscriptタグとして埋め込み。私は評価項目が多いので、部分テンプレートにて切り出しました。 <script> $('#star1').raty({ size : 38, //星のサイズ starOff: '<%= asset_path('star-off.png') %>', //imagesフォルダより星画像の呼び出し starOn : '<%= asset_path('star-on.png') %>', starHalf: '<%= asset_path('star-half.png') %>', scoreName: 'review[comfort]', //reviewモデルのcomfortカラムに保存 half: true, //星半分を許可する。DBには0.5単位の数値が保存される }); </script> これで、保存するとDB上では小数を含む数値にて保存がなされました。 保存した評価の表示 ビューファイルの書き方 <div class="star-group"> <div class="detail-review">乗り心地</div> <div class="detail-value", id="star-comf-<%= @review.id %>"></div> <%# id要素の書き方がポイント %> <div class="eval-star"><%= @review.comfort %>/5点</div> <%# @モデル.カラムで保存された数値を表示 %> </div> ratyを用いた星評価の表示 <script> $('#star-comf-<%= @review.id %>').raty({ //.idでどの評価であるかを取得 size: 38, starOff: '<%= asset_path('star-off.png') %>', starOn : '<%= asset_path('star-on.png') %>', starHalf: '<%= asset_path('star-half.png') %>', half: true, readOnly: true, //readOnlyオプションで読み込み専用とする。編集できない。 score: <%= @review.comfort %>, //scoreオプションで評価内容を取得 }); </script> これでレビューの詳細ページなどに適用すれば、星評価が一目瞭然ですね。 再評価・編集 ビューファイルの書き方 基本的には評価の保存時の書き方と同じ。 <div class="star-edit-group"> <%= f.label :comfort,'乗り心地', class:'star-title' %> <div class="detail-value", id="edit-comf-<%= @review.id %>"></div> <%# .idでどの評価であるかを取得 %> </div> ratyを用いた再評価 <script> $('#edit-comf-<%= @review.id %>').raty({ size: 36, starOff: '<%= asset_path('star-off.png') %>', starOn : '<%= asset_path('star-on.png') %>', starHalf: '<%= asset_path('star-half.png') %>', half: true, score: <%= @review.comfort %>, //scoreオプションで最初の評価内容を取得 scoreName: 'review[comfort]', //scoreNameオプションで、新たに評価を保存 }); </script> これで最初に評価した内容が星で表示され、かつ再評価が可能で、新たに評価内容が保存できるようになりました。 まとめ 評価→表示→再評価といった一連の流れをまとめることができてよかったです。 今回は基礎的な部分のみの実装でしたが、改めて公式GitHubを見るとこれ以外にもたくさんオプションがありますので、応用が効きそうです。 余力があれば、評価の際にマウスオーバーした時に星の横にでも数値化された値が表示されるようになればより優れたUIになりそうなので、挑戦してみたいですね。 余談ですが、今回初めてGIFの埋め込みをしてみたのですが、縦横比があまりよろしくなく見にくくてすみません・・・。 ご覧いただきありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

seed_fu使ってシードデータを作ったのにrspecのテストを回すとシードデータが消えている

rspec使ったテストを書き始めたので,github actionsでのCI環境でもテストを回るようにしたところ... rspecを実行するとシードデータが消えていることがわかりました. 原因 spec/rails_helper.rb にある以下の記述が原因みたいです. これはテストが始まる時にmigrationがちゃんとできてるかどうか確認してデータベースのスキーマをschema.rbに合わせるみたいです. もしmigrationを行う場合,DBを綺麗に消しちゃうみたいで,それによってseed作っても消えちゃってた様です. begin ActiveRecord::Migration.maintain_test_schema! rescue ActiveRecord::PendingMigrationError => e puts e.to_s.strip exit 1 end 解決策1: seedをrspec実行時に作るようにする spec/spec_helper.rb に以下を追記するとよいです. config.before(:suite) はrspecを実行する前に1回だけ行う処理をかけるみたいです. RSpec.configure do |config| ... config.before(:suite) do SeedFu.seed end ... end 解決策2: Seedを作っても消えないようにする ActiveRecord::Migration.maintain_test_schema! を無効にします. config/environment/test.rb に以下の1行を追加します. config.active_record.maintain_test_schema = false この場合,migrationをRAILS_ENV=test と指定して回す必要もあります. CIなどの一連の流れはこのような感じに(github actionsなどではもっと楽にenv を指定するだけでOKです) RAILS_ENV=test rails db:setup RAILS_ENV=test rails db:seed_fu RAILS_ENV=test bundle exec rspec 参考 [http://katsuyukikun.hatenablog.com/entry/2018/07/24/224251:embed:cite] [https://qiita.com/syunk38/items/e6cc1282c5479555f748#suite:embed:cite]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

active hashを使う場合のバリデーション

active hashを使ってプルダウンを実装する場合、プルダウンの一番上を --- みたいにすると思う。 例えば、 class Genre < ActiveHash::Base self.data = [ { id: 0, genre_id: '--' }, { id: 1, genre_id: '悩み' }, { id: 2, genre_id: 'エラー' }, { id: 3, genre_id: '技術内容' }, { id: 4, genre_id: '恋愛' }, { id: 5, genre_id: '人間関係' }, { id: 6, genre_id: 'キャリア' }, { id: 7, genre_id: '家庭' }, { id: 8, genre_id: '体調' }, { id: 9, genre_id: '自己実現' }, { id: 10, genre_id: 'その他' } ] include ActiveHash::Associations has_many :articles end このようなコードの場合、id=0のデータは、テーブルに保存したくないはず。 そのようなバリデーションを設定するには、 with_options numericality: { other_than: 0 } do validates :genre_id end このように、thanの指定を、保存したくないidを指定することで、テーブルに保存ができないようにバリデーションをしてくれる。 参考にして頂ければ、幸いである。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

cloud9のrails周りの環境構築メモ

Yarn Rails6をインストールするのに必要 $ npm install -g yarn Ruby デフォルトのバージョンだと、Herokuにデプロイできなかったりする。新しめのバージョンにアップデート。 cloud9ではデフォルトで、rbenvではなくrvmを使ってRubyのバージョンが管理されている。 アンストしてrbenvを入れ直してもいいけど、ここでは最小手数で行きたいのでrvmを使う。 $ echo rvm_autoupdate_flag=2 >> ~/.rvmrc # rvmが常に最新バージョンになるよう設定 $ rvm -v $ rvm get latest # rvmのバージョンを最新版に更新 $ rvm -v $ rvm list # インストール済みのRubyバージョンを一覧表示 =* ruby-2.6.3 [ x86_64 ] $ rvm list known # インストール可能なRubyバージョンを一覧表示 $ rvm install 3.0.0 Rails $ gem install rails -v 6.1.3.1 $ rails -v ImageMagick $ sudo yum -y install libpng-devel libjpeg-devel libtiff-devel gcc $ cd $ git clone https://github.com/ImageMagick/ImageMagick.git ImageMagick-7.0.11 $ cd ImageMagick-7.0.11 $ ./configure $ make $ sudo make install Heroku CLI $ curl -OL https://cli-assets.heroku.com/heroku-linux-x64.tar.gz $ tar zxf heroku-linux-x64.tar.gz && rm -f heroku-linux-x64.tar.gz $ sudo mv heroku /usr/local $ echo 'PATH=/usr/local/heroku/bin:$PATH' >> $HOME/.bash_profile $ source $HOME/.bash_profile > /dev/null $ heroku -v $ heroku login --interactive
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

よく使用するコマンドまとめ

はじめに よく使うコマンドをまとめてみました。 他にも便利なコマンドがありましたら、その都度更新していきます。 Git ファイル変更確認 git status ステージング git add . git add ファイル名 .で全てステージングする ファイル名で変更ファイルのみステージングする コミット git commit -m "メッセージ" プッシュ git push origin -u ブランチ名 -uを使用することで次回から git pushでpush可能 マージ git merge 変更したブランチ名 リモートの変更を取り込む(fetch) git fetch リモートの内容がローカルより進んでいる場合に使用する。 fetchすると、ローカル状のorigin/masterに変更が保存されるため、反映するにはセットでgit merge masterの作業が必要である。 リモートの変更を一回で取り込む(pull) git pull origin master fetchとmergeを一括で行う方法。 リモートの変更を取り込んでpushする git pull origin develop pullをpushを一回で行う方法。 ブランチを作成し、移動 git checkout -b ブランチ名 ブランチ削除 git branch -d ブランチ名 git branch -D ブランチ名 変更があった場合でも削除したい時は「-D」 masterブランチ以外一括削除 git branch | grep -v master | xargs git branch -D https://qiita.com/takat0-h0rikosh1/items/766e207ba1c799ed1375 特定のコミットまで戻る git reset --hard ハッシュ値 https://qiita.com/Yorinton/items/e0e969d961b17a359e19 直前のコミットメッセージを変更 追記2021/4/20 git commit --amend -m"コミットメッセージ" コミットログ確認 追記2021/4/20 git log git log --oneline onelineでコミットメッセージのみ表示 Rails ルーティング確認 rails routes GemfileとGemfile.lockの差分をインストール bundle install Gemfileを全てインストール bundle update gemfile.lockの情報関係なく1からgemfileをインストールする。 Rails Console DBに保存しないでconsoleを使用する時 rails c -s →rails console sandboxの略 テーブルのカラムを確認する時 モデル名.column_names
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuでファイルの変更履歴が存在しない場合にプッシュする方法

ファイルの変更履歴が存在しない場合、特に変更するファイルもないがHerokuに設定した環境変数だけ本番環境に反映させたい場合などはどうすればいいでしょうか。 最新のコミット履歴が存在しない状態でgit push heroku masterコマンドを実行しても「Everything up-to-date」と表示されるだけで何も起きません。 そこでターミナルで下記のコマンドを実行して空のコミットを生成することで解決できます。 ターミナル % git commit --allow-empty -m "空のcommit" これでGitHub Desktopで空のコミット履歴ができたのでHerokuへプッシュすることができます。 ターミナル % git push heroku master あとはいつもの上記のコマンドを実行すれば問題なく反映させることができます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsの本番環境にruby3.0の型定義を入れていく

やったこと 自分の持ち手の中に以下の三拍子が揃ったプロダクトがあったのでruby 3.0まであげて強い気持ちで型を入れていくことにしました。 立ち上げ間もない(利用者も限定的) 各種ライブラリが最新(変な古いバージョンに影響されることが少ない) 今後そこそこの機能拡張が見込まれる うちのチームでは初めてのRuby 3.0&型付だったけどやっていき 結論成果物 tool rbs Steep API周りのモデル層に型定義を導入 circleciによる自動テストの追加 Rubyの型とは Rubyの静的解析はv3.0で導入された新機能になります。 型定義はrubyのコードの外側にRBSファイルとして定義していきます。 2010年代は静的型言語の時代でした。Rubyは抽象解釈を武器に、型宣言なしで静的型チェックする未来を目指します。RBSとTypeProfはその第一歩です。Rubyがもたらす誰も見たことがない静的型の世界を見守ってください — Matz 引用: https://www.ruby-lang.org/ja/news/2020/12/25/ruby-3-0-0-released/ Rubyの型定義周りでいくつかのツールが出てきてこんがらがりやすいのでさっくりまとめます。 ここら辺は クックパッドさんの開発者ブログ が詳しく書かれていました。 RBS Rubyの型定義を行うための言語。Ruby 3 にバンドルされています TypeProf Rubyのコードから型を解析してRBSファイルを出力するためのツール。Ruby 3 にバンドルされる。 Steep/Sorbet Rubyの型チェックの実施やIDEで型の表示やリアルタイムで型の確認などをしてくれるツール。 さらにRailsや本番環境に型を導入するにはここら辺も必要になってきました。 gem_rbs_collection 各種gemのrbsファイルをよしなに集約してくれるgem rbs_rails Railsの各種機能のrbsを提供してくれたり、ActiveRecordやurl_helper周りの定義ファイルの作成タスクを提供してくれている rspecで書いたテストからTypeProf通して自動生成とかしてくれないかなぁ。。。という希望(見つからなかった) 導入していく 必要なライブラリの導入 一旦各種ライブラリを導入 # Gemfile group :development, :test do gem 'rbs_rails', require: false gem 'steep', require: false gem 'rbs', require: false end gem_rbs_collection導入 どこかにおいてプロダクト間で同じものを利用してもいいのですが 各々のPCでgem_rbs_collectionを置く場所を強制する 別プロダクトで参照するgem_rbs_collectionの場所を強制する のようなお気持ちがなかったのでサブモジュールとしてプロダクト配下に追加 $ git submodule add https://github.com/ruby/gem_rbs_collection.git gem_rbs/gems Steepfileの用意 Steepfileを一旦作成(余計なものも入っているかもしれないですが一旦導入することを優先) target :app do signature 'sig' # => 型定義ファイルをおくディレクトリ check 'app' # => チェック対象ディレクトリ(最終的にはmodelsの特定のディレクトリ(API関連のロジック)だけに絞りました...) repo_path "gem_rbs/gems" # => submoduleで追加したディレクトリ library 'pathname' library 'logger' library 'mutex_m' library 'date' library 'monitor' library 'singleton' library 'tsort' library 'activesupport' library 'actionpack' library 'activejob' library 'activemodel' library 'actionview' library 'activerecord' library 'railties' end rbs_railsのセットアップ こちらの通りに進めていく https://github.com/pocke/rbs_rails#installation # lib/tasks/rbs.rake require 'rbs_rails/rake_task' RbsRails::RakeTask.new タスク実行! $ bundle exec rbs_rails:all ActiveRecordやら各種url_helper周りの型定義ファイルが出てきた。。。。つよぃ。。。 circleciによる自動テスト 実際にrspecでテストしているところに入れていくので細かいところははしょります git submoduleで追加したgem_rbs_collectionを更新する commands: ... install_submodule: description: install submodule steps: - run: name: git submodule init command: git submodule init - run: name: git submodule update command: git submodule update job用意 jobs: steep: executor: name: default steps: - checkout - setup_something # bundle install etc - install_submodule - run: name: run steep command: bundle exec steep check workflowに追加 workflows: version: 2 build-and-deploy: jobs: - steep - rspec # (既存のもの) (実際には真っ赤になりますが無事に型検証が通った記念) 実際のプロダクトに入れていく まずスコープを絞った 出てきたエラー件数を見て一旦対象を外部露出しているAPIのロジック部分に絞りました。 target :app do ... check 'app/models/api' ... end ARを拡張している層の型定義はgeneratorに寄せて自動生成 このプロダクトではARをラッピングした層を用意しており、基本的なメソッドはARにdelegateして特定のメソッドを拡張できるようにしていました。 この層ではmethod_missingをフックに自前定義していないものはARにdelegateする機構を組んでいて、ARで提供しているメソッドも提供しています。 こちらに対してissueがありましたがどうにも上手いやり方はない様子... https://github.com/ruby/rbs/issues/422 最初はよく使われているものだけ共通層に定義すればいいかなと思ったのですが、個別モデルの事情(特にRelation周り)によったものを都度定義するのはだいぶしんどかったので最終的にgeneratorを自分で作ることにしました。 幸い rbs_rails でActiveRecordに対する型定義を自動生成していたので多分に参考にさせてもらいながらシンプルを保てる範囲で自動型定義ファイルの出力をするようにしました。 一つのclassに対して複数のrbsファイルで分けても大丈夫と言うことなのでrbs_railsと同様自前のディレクトリを用意してそこにrbsファイルを出力するようにしました。 これによって実際の自前定義したビジネスロジック部分だけを型付けしていけばよくなったのでだいぶやる気が上がりました Steepのバグ?に当たってciが通らない 継承元のメソッドに対してsuper(**args, &block)と渡した時にblock optionalで宣言した型定義が通らない問題に直面し 悩んだ挙句ruby-jpにお尋ねした。 https://ruby-jp.slack.com/archives/CM3PA3DAB/p1618535504180600 結果Steepのバグの可能性が出てきた。 そこでどうしてもクリアできないところへの対処を教えてもらいました。 __skip__ = begin dosomething # この中ではSteepによる検査がスキップされる end 結果 キタ――(゚∀゚)――!! 参考 ruby3.0 release note クックパッド開発者ブログ - Ruby 3の静的解析機能のRBS、TypeProf、Steep、Sorbetの関係についてのノート pockestrap - RBS Railsを使ってRailsアプリケーションにSteepを導入する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Steep]Railsの本番環境にruby3.0の型定義を入れていく

やったこと 自分の持ち手の中に以下の三拍子が揃ったプロダクトがあったのでruby 3.0まであげて強い気持ちで型を入れていくことにしました。 立ち上げ間もない(利用者も限定的) 各種ライブラリが最新(変な古いバージョンに影響されることが少ない) 今後そこそこの機能拡張が見込まれる うちのチームでは初めてのRuby 3.0&型付だったけどやっていき 結論成果物 tool rbs Steep API周りのモデル層に型定義を導入 circleciによる自動テストの追加 Rubyの型とは Rubyの静的解析はv3.0で導入された新機能になります。 型定義はrubyのコードの外側にRBSファイルとして定義していきます。 2010年代は静的型言語の時代でした。Rubyは抽象解釈を武器に、型宣言なしで静的型チェックする未来を目指します。RBSとTypeProfはその第一歩です。Rubyがもたらす誰も見たことがない静的型の世界を見守ってください — Matz 引用: https://www.ruby-lang.org/ja/news/2020/12/25/ruby-3-0-0-released/ Rubyの型定義周りでいくつかのツールが出てきてこんがらがりやすいのでさっくりまとめます。 ここら辺は クックパッドさんの開発者ブログ が詳しく書かれていました。 RBS Rubyの型定義を行うための言語。Ruby 3 にバンドルされています TypeProf Rubyのコードから型を解析してRBSファイルを出力するためのツール。Ruby 3 にバンドルされる。 Steep/Sorbet Rubyの型チェックの実施やIDEで型の表示やリアルタイムで型の確認などをしてくれるツール。 さらにRailsや本番環境に型を導入するにはここら辺も必要になってきました。 gem_rbs_collection 各種gemのrbsファイルをよしなに集約してくれるgem rbs_rails Railsの各種機能のrbsを提供してくれたり、ActiveRecordやurl_helper周りの定義ファイルの作成タスクを提供してくれている rspecで書いたテストからTypeProf通して自動生成とかしてくれないかなぁ。。。という希望(見つからなかった) 導入していく 必要なライブラリの導入 一旦各種ライブラリを導入 # Gemfile group :development, :test do gem 'rbs_rails', require: false gem 'steep', require: false gem 'rbs', require: false end gem_rbs_collection導入 どこかにおいてプロダクト間で同じものを利用してもいいのですが 各々のPCでgem_rbs_collectionを置く場所を強制する 別プロダクトで参照するgem_rbs_collectionの場所を強制する のようなお気持ちがなかったのでサブモジュールとしてプロダクト配下に追加 $ git submodule add https://github.com/ruby/gem_rbs_collection.git gem_rbs/gems Steepfileの用意 Steepfileを一旦作成(余計なものも入っているかもしれないですが一旦導入することを優先) target :app do signature 'sig' # => 型定義ファイルをおくディレクトリ check 'app' # => チェック対象ディレクトリ(最終的にはmodelsの特定のディレクトリ(API関連のロジック)だけに絞りました...) repo_path "gem_rbs/gems" # => submoduleで追加したディレクトリ library 'pathname' library 'logger' library 'mutex_m' library 'date' library 'monitor' library 'singleton' library 'tsort' library 'activesupport' library 'actionpack' library 'activejob' library 'activemodel' library 'actionview' library 'activerecord' library 'railties' end rbs_railsのセットアップ こちらの通りに進めていく https://github.com/pocke/rbs_rails#installation # lib/tasks/rbs.rake require 'rbs_rails/rake_task' RbsRails::RakeTask.new タスク実行! $ bundle exec rbs_rails:all ActiveRecordやら各種url_helper周りの型定義ファイルが出てきた。。。。つよぃ。。。 circleciによる自動テスト 実際にrspecでテストしているところに入れていくので細かいところははしょります git submoduleで追加したgem_rbs_collectionを更新する commands: ... install_submodule: description: install submodule steps: - run: name: git submodule init command: git submodule init - run: name: git submodule update command: git submodule update job用意 jobs: steep: executor: name: default steps: - checkout - setup_something # bundle install etc - install_submodule - run: name: run steep command: bundle exec steep check workflowに追加 workflows: version: 2 build-and-deploy: jobs: - steep - rspec # (既存のもの) (実際には真っ赤になりますが無事に型検証が通った記念) 実際のプロダクトに入れていく まずスコープを絞った 出てきたエラー件数を見て一旦対象を外部露出しているAPIのロジック部分に絞りました。 target :app do ... check 'app/models/api' ... end ARを拡張している層の型定義はgeneratorに寄せて自動生成 このプロダクトではARをラッピングした層を用意しており、基本的なメソッドはARにdelegateして特定のメソッドを拡張できるようにしていました。 この層ではmethod_missingをフックに自前定義していないものはARにdelegateする機構を組んでいて、ARで提供しているメソッドも提供しています。 こちらに対してissueがありましたがどうにも上手いやり方はない様子... https://github.com/ruby/rbs/issues/422 最初はよく使われているものだけ共通層に定義すればいいかなと思ったのですが、個別モデルの事情(特にRelation周り)によったものを都度定義するのはだいぶしんどかったので最終的にgeneratorを自分で作ることにしました。 幸い rbs_rails でActiveRecordに対する型定義を自動生成していたので多分に参考にさせてもらいながらシンプルを保てる範囲で自動型定義ファイルの出力をするようにしました。 一つのclassに対して複数のrbsファイルで分けても大丈夫と言うことなのでrbs_railsと同様自前のディレクトリを用意してそこにrbsファイルを出力するようにしました。 これによって実際の自前定義したビジネスロジック部分だけを型付けしていけばよくなったのでだいぶやる気が上がりました Steepのバグ?に当たってciが通らない 継承元のメソッドに対してsuper(**args, &block)と渡した時にblock optionalで宣言した型定義が通らない問題に直面し 悩んだ挙句ruby-jpにお尋ねした。 https://ruby-jp.slack.com/archives/CM3PA3DAB/p1618535504180600 結果Steepのバグの可能性が出てきた。 そこでどうしてもクリアできないところへの対処を教えてもらいました。 __skip__ = begin dosomething # この中ではSteepによる検査がスキップされる end 結果 キタ――(゚∀゚)――!! 参考 ruby3.0 release note クックパッド開発者ブログ - Ruby 3の静的解析機能のRBS、TypeProf、Steep、Sorbetの関係についてのノート pockestrap - RBS Railsを使ってRailsアプリケーションにSteepを導入する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

system_specで直接javascriptを実行する

はじめに system specを書いてるときに、javascriptが起動しないときがあり直接javascriptを実行できたらエラーを回避できるときがあると思います。 解決法 capybaraのexecute_scriptを使うとjavascriptを実行させることができます。 page.excute_script "console.log('javascript')" これを利用すると、強引にフォームに値を送り込めます。 page.excute_script "$('#form').val('フォームの値')" 本来は、エラーを解消してテストをパスするべきなので利用はご利用的に
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】画像投稿機能において画像を投稿していないときに代替画像を表示させる方法

前提 ・CarrierWaveは導入済み ・登録した画像表示はできる はじめに 画像投稿機能において画像を投稿していないときに代替画像を表示させる方法を記します。 基本的にはCarrierWaveを使ってデフォルト画像を設定すれば画像を投稿していないときに代替の画像を表示させることができます。ただ、アップロードする必要がある画像部分が全部同じデフォルト画像になってしまいます。アップロードにより表示させる画像に見境なく用意した画像を使用してしまう為、表示にそぐわない部分にも適用されてしまう可能性があります。 if文を使って画像を投稿していないときに代替画像を表示させることで、上記の問題を解決したので自らのメモとして残しておきます。 手順 まず、image_tagで画像を表示させるコードを記述。 <% @posts.each do |p| %>             <%= image_tag p.image_url %>             <% end %> そして、代替画像を表示させるコードも準備します。 <%= image_tag "no_imageとか書かれた代替画像" %> そして下記のようにif文で記述する。 <% if p.image.present? %> <%= image_tag p.image_url %> <% else %> <%= image_tag "no_imageとか書かれた代替画像" %> <% end %> presentメソッドを使って、p.imageに画像が入っていればユーザーから投稿された画像を表示し、画像が入っていなければこちら側が準備した代替画像を表示させるようにしています。 参考記事 【Rails】image_tagの基本とデフォルト画像の設定方法
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails6】共感(いいね)機能を、JavaScriptでAPIにfetchでリクエストを送って非同期通信(Ajax)で実装してみた

はじめに なぜこの記事を書くことにしたのか? jQueryを使用したAjaxの実装方法はあったのですが、素のJavaScriptでAPIにfetchでリクエストを送る実装方法が、ネット上であまり見受けられなかったからです。 私のようにJavaScriptで1から実装したいと考えている方の参考になればと思います。 私のポートフォリオでは、いいね機能のことを共感機能と名付けているので、これより下の説明では共感機能と呼びます。 共感機能の仕様を考える 画面のレイアウト・動作 投稿にある共感ボタンを押すと、色とテキストが変化します。 使用する言語・フレームワーク Ruby 3.0.0 Rails 6.1.3 MySQL 8.2.3 tailwindcss 1.9.0 データベースのテーブル設計 今回関係あるのは、usersテーブル、empathiesテーブル、postsテーブルです。 usersテーブルはユーザーを表しています。 postsテーブルは投稿を表しています。 empathiesは中間テーブルです。 テーブル名 カラム名 カラム名 カラム名 カラム名 users id nickname email password posts id text user_id password empathies id user_id post_id 共感機能の流れ 流れ 共感機能は、共感ボタンを押すことで、JavaScriptが作動し、共感ボタンの色とテキストを変化させます。そして、JavaScriptはサーバーと非同期通信を行い、データをRails側に渡しに行きます。Railsでは、データベースに必要な情報を保存・削除します。 以上が、大まかな共感機能の流れとなります。 では、非同期通信とは何なのか説明する前に、同期通信について説明します。 同期通信とは クライアントとサーバーが交互に処理を行い、同調して通信を行うことを同期通信と呼びます。同期通信の場合、サーバーが処理を待っている間、クライアントは待つことしかできず、HTMLファイルを受け取ってから表示の処理を行うため、全体としてページの更新に時間がかかってしまいます。また、送信するデータも多くなりがちで、サーバーに負担がかかってしまいます。 非同期通信とは 非同期通信はAjaxをも呼ばれています。Ajaxは同期通信の欠点を補うために誕生しました。AjaxではWebブラウザ上で、クライアントサイド・スクリプトとして動くJavaScriptが直接Webサーバーと通信を行い、取得したデータを用いて、表示するHTMLを更新します。HTMLそのものをやり取りするのではなく、更新に必要なデータのみをやりとりするため、送信するデータ量は同期通信と比べて少ないため、サーバーへの負担が抑えられます。 ルーティングの設定 共感ボタンをクリックした時に、JavaScriptにjson形式で値を渡せるように定義します。 config/routes.rb Rails.application.routes.draw do root to: "posts#index" devise_for :users resources :posts namespace :api, format: :json do namespace :v1 do resources :empathies, only: [:create, :destroy] end end end モデルの設定 app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :posts, dependent: :destroy has_many :empathies, dependent: :destroy end app/models/post.rb class Post < ApplicationRecord belongs_to :user has_many :empathies, dependent: :destroy # postのuserに関するempathyレコードを取得する def empathy_by(user) empathies.where(empathies: { user_id: user }).last end # userが共感しているかチェックしている def empathy_by?(user) empathy_by(user).present? end end app/models/empathy.rb class Empathy < ApplicationRecord belongs_to :user belongs_to :post EMPATHY_COLOR = "inline-block border border-red-500 py-1 px-2 rounded-lg text-white bg-red-500".freeze UNEMPATHY_COLOR = "inline-block border border-red-500 py-1 px-2 rounded-lg text-red-500 bg-white".freeze end 「EMPATHY_COLOR」と「UNEMPATHY_COLOR」ですが、tailwindcssを使っているので、class名でcssを設定しています。これは、後ほどビューで使用するので、あらかじめモデル内で定義しています。 ビューの設定 共感ボタンを様々なページで使いまわしたので、パーシャルテンプレートを使います。 app/views/empathies/_empathies.html.erb <% if user_signed_in? %> <% empathy_button_color = post.empathy_by?(current_user) ? Empathy::EMPATHY_COLOR : Empathy::UNEMPATHY_COLOR %> <% if post.empathy_by?(current_user) %> <button class="js-empathy-button <%= empathy_button_color %>" id="<%= post.id %>" value="<%= post.empathy_by(current_user).id %>">共感済み</button> <% else %> <button class="js-empathy-button <%= empathy_button_color %>" id="<%= post.id %>" value=" ">共感する</button> <% end %> <% end %> empathy_button_color = post.empathy_by?(current_user) ? Empathy::EMPATHY_COLOR : Empathy::UNEMPATHY_COLOR この部分ですが、「AAA ? BBB : CCC」を使用しています。 これは、AAAという条件に該当する場合はBBBを実行、該当しない場合はCCCを実行するという意味です。 つまり、postにすでに共感していればEMPATHY_COLORを代入して、まだ共感していなければUNEMPATHY_COLORを代入するという意味です。 jsファイルの設定 app/javascript/js/empathies.js document.addEventListener('turbolinks:load', () => { const empathyColor = "js-empathy-button inline-block border border-red-500 py-1 px-2 rounded-lg text-white bg-red-500"; const unempathyColor = "js-empathy-button inline-block border border-red-500 py-1 px-2 rounded-lg text-red-500 bg-white"; const empathyEndpoint = '/api/v1/empathies'; const getCsrfToken = () => { const metas = document.getElementsByTagName('meta'); for (let meta of metas) { if (meta.getAttribute('name') === 'csrf-token') { return meta.getAttribute('content'); } } return ''; } const sendRequest = async (endpoint, method, json) => { const response = await fetch(endpoint, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-Token': getCsrfToken() }, method: method, credentials: 'same-origin', body: JSON.stringify(json) }); if (!response.ok) { throw Error(response.statusText); } else { return response.json(); } } const empathyButtons = document.getElementsByClassName('js-empathy-button'); // postの一覧ページで複数要素がある時に対応できるようにfor文を使っている for (let i = 0; i < empathyButtons.length; i++) { // 共感ボタンをクリックしたときの処理 empathyButtons[i].addEventListener('click', event => { const button = event.target; const createEmpathy = (postId, button) => { sendRequest(empathyEndpoint, 'POST', { post_id: postId }) .then((data) => { button.value = data.empathy_id console.log(button.value); }); } const deleteEmpathy = (empathyId, button) => { const deleteEmpathyEndpoint = empathyEndpoint + '/' + `${empathyId}`; sendRequest(deleteEmpathyEndpoint, 'DELETE', { id: empathyId }) .then(() => { button.value = ''; console.log(button.value); }); } if (!!button) { const currentColor = button.className; const postId = button.id; const empathyId = button.value; // 共感する場合 if (currentColor === unempathyColor) { button.className = empathyColor; button.innerText = '共感済み'; createEmpathy(postId, button); } // 共感済みの場合 else { button.className = unempathyColor; button.innerText = '共感する'; deleteEmpathy(empathyId, button); } } }); } }); 記述がとても長いので、分割して説明します。 const empathyColor = "js-empathy-button inline-block border border-red-500 py-1 px-2 rounded-lg text-white bg-red-500"; const unempathyColor = "js-empathy-button inline-block border border-red-500 py-1 px-2 rounded-lg text-red-500 bg-white"; const empathyEndpoint = '/api/v1/empathies'; const getCsrfToken = () => { const metas = document.getElementsByTagName('meta'); for (let meta of metas) { if (meta.getAttribute('name') === 'csrf-token') { return meta.getAttribute('content'); } } return ''; } empathyColorとunempathyColorで、共感ボタンのスタイルを定義しています。tailwindを使っているので、後でclass名を上書きするのに使います。 empathyEndpointはRails側でどのコントローラーを使うのかを設定しています。こちらは、後でfetchメソッドというのが登場してくるのですが、その時にどのurlにデータを送信するのかを設定する時に使います。なので、あらかじめ設定しておきます。 getCsrfTokenですが、こちらを設定しておかないとエラーになってしまいます。なぜかというと、Railsの仕様で、app/views/layouts/application.html.erbに最初から書かれている<%= csrf_meta_tags %> などによって GET以外のあらゆる非同期通信Requestでは正しいX-CSRF-TokenをRequest Headerに含めないと サーバー側はRequestを弾くようにしているためです。クロスサイトリクエストフォージェリ(CSRF)というサイバー攻撃対策用のTokenを用いた仕組みです。以下に参考にしたサイトを張っていおきます。 const sendRequest = async (endpoint, method, json) => { const response = await fetch(endpoint, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-Token': getCsrfToken() }, method: method, credentials: 'same-origin', body: JSON.stringify(json) }); if (!response.ok) { throw Error(response.statusText); } else { return response.json(); } } こちらでは、どのurl(endpoint)に、どのHTTPメソッド(method)で、どんなデータ(json)を送信するのかというの設定しています。 このコードの理解を深めるためには、fetch,async,awaitの使い方をしる必要があります。以下に参考になるサイトを貼るっていおきます。 // 画面上のボタン要素を全て取得する const empathyButtons = document.getElementsByClassName('js-empathy-button'); // postの一覧ページで複数要素がある時に対応できるようにfor文を使っている for (let i = 0; i < empathyButtons.length; i++) { // 共感ボタンをクリックしたときの処理 empathyButtons[i].addEventListener('click', event => { const button = event.target; const createEmpathy = (postId, button) => { sendRequest(empathyEndpoint, 'POST', { post_id: postId }) .then((data) => { button.value = data.empathy_id }); } const deleteEmpathy = (empathyId, button) => { const deleteEmpathyEndpoint = empathyEndpoint + '/' + `${empathyId}`; sendRequest(deleteEmpathyEndpoint, 'DELETE', { id: empathyId }) .then(() => { button.value = ''; }); } if (!!button) { const currentColor = button.className; const postId = button.id; const empathyId = button.value; // 共感する場合 if (currentColor === unempathyColor) { button.className = empathyColor; button.innerText = '共感済み'; createEmpathy(postId, button); } // 共感済みの場合 else { button.className = unempathyColor; button.innerText = '共感する'; deleteEmpathy(empathyId, button); } } }); } まず、画面上のボタンの要素をempathyButtonsに代入します。 画面上に投稿が一つだけの場合は、いきなりaddEventListenerのclickを使用しても問題ないのですが、画面上に複数投稿ある場合は、一旦全ての要素を取得してからでないと、うまく作動してくれません。なので、for文を使用しています。 そして、for文で一つ一つの要素にクリックした時にイベントが発火するようにしていきます。 「const button = event.target;」でクリックしたボタンをbuttonに代入しています。 createEmpathyでは、JavaScriptからRailsのempathiesコントローラーのcreateアクションにリクエストを送信し、送信されたデータを元に新たにempathyレコードが作成され、その作成されたレコードのidを受け取り、buttonタグのbalueに挿入します。 deleteEmpathyでは、JavaScriptからRailsのempathiesコントローラーのdestroyアクションにリクエストを送信し、されたデータを元にemapathyレコードを削除し、無事に削除が完了したら、buttonタグのvalueを空にします。 「!!button」は二重否定を使うことで、trueを返すようにしています。 それより以下は、ボタンのスタイルの上書きを行い、テキストの上書きを行い、それぞれの関数を実行するという感じです。 作成したjsファイルの読み込み方 以下のように記述してあげることで読み込みが完了します。 app/javascript/packs/application.js import "../js/empathies" まとめ 以上でJavaScriptでAPIにfetchでリクエストを送る実装は終了となります。 お疲れさまでした。 参考にしたサイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ストロングパラメーターparam is missing or the value is empty:について

今回問題となる箇所 def create customer = Customer.new(customer_params) if customer.save render json: { status: 'success11111', data: customer } else render json: { status: 'error', data: customer.errors } end end private def customer_params params.require(:customer).permit(:name, :age, :sex, :memo) end です。 どういった問題かといいますと param is missing or the value is empty: パラメーターがないか、値が空であるといった内容でした。 今回の場合はapiの実装中ということで、Postmanを利用し、リクエストに対するレスポンスの確認を行っていました。 こんな感じです。 いろいろ調べてみた結果、 def create customer = Customer.new(customer_params) if customer.save render json: { status: 'success11111', data: customer } else render json: { status: 'error', data: customer.errors } end end private def customer_params params.permit(:name, :age, :sex, :memo) end require(:customer)を消せばエラー無くレスポンスがかえってきました。 原因はおそらく controller側で@customer = Collect.newやなどの書き忘れが原因なことが多いようです! <%= form_with model: Customer.new do %> あるいはcontroller側で@collect = Collect.newとして <%= form_with model: @customer do % とした場合、パラメータは <ActionController::Parameters {(略),"url"=>"https://(略)", "customer"=>{"group_id"=>"5", "commit"=>"Save ", "controller"=>"customer"}, "action"=>"create"} permitted: true> となり、ストロングパラメータは正常に実行されるはずです 今回の場合はPostmanにてリクエストを送信していて、 <%= form_with model: @customer, url: customers_path, method: :post do %> といったようにviewファイルからモデルを指定して送られたリクエストではないため、param is missing or the value is empty:が起きたと考えられます! 今回私はviewファイルを作成していない段階の事例でしたが、最初にも記述しましたがこのエラーに関しての多くは@customer = Collect.newなどの書き忘れが原因なことが多かったり、複数形のスペルミスだったりするようなので、チェックしてみてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでスワイプで画像切り替えする方法

前提 Ruby 2.6.3 Rails 6.i.3 Font Awesome導入済み Bootstrapの導入 $ yarn add jquery bootstrap popper.js インストールが出来たらenvironment.jsに以下の文を記述してください config/webpack/environment.js const webpack = require('webpack') environment.plugins.append( 'Provide', new webpack.ProvidePlugin({ $: 'jquery/src/jquery', jQuery: 'jquery/src/jquery', Popper: ['popper.js', 'default'] }) ) application.jsにも追記してください。 app/javascript/packs/application.js import 'bootstrap'; import '../stylesheets/application'; Bootstrap導入出来ているはずです。 試しにボタンとか追加して確認してください。 Bootstrap公式ページ application.scss作成 mkdir app/javascript/stylesheets touch app/javascript/stylesheets/application.scss application.html.erb追加 app/views/layouts/application.html.erb <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %> Hammer.js の導入 参考ドキュメント $ yarn add hammerjs package.json { "name": "match", "private": true, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.3", "@rails/actioncable": "^6.0.0", "@rails/activestorage": "^6.0.0", "@rails/ujs": "^6.0.0", "@rails/webpacker": "5.2.1", "bootstrap": "^4.6.0", "hammerjs": "^2.0.8", //追記されてたらインストールできてます。 "jquery": "^3.6.0", "popper.js": "^1.16.1", "turbolinks": "^5.2.0" }, "version": "0.1.0", "devDependencies": { "webpack-dev-server": "^3.11.2" } } packs/application.jsに追加 app/javascript/packs/application.js import 'hammerjs'; //...(省略) require("src/swipe") swipe.js作成 touch app/javascript/src/swipe.jsを作成します。 app/javascript/src/swipe.js if (location.pathname == "/users") { $(function () { let allCards = document.querySelectorAll(".swipe--card"); let swipeContainer = document.querySelector(".swipe"); function initCards() { let newCards = document.querySelectorAll(".swipe--card:not(.removed)"); newCards.forEach(function (card, index) { card.style.zIndex = allCards.length - index; card.style.transform = "scale(" + (20 - index) / 20 + ") translateY(-" + 30 * index + "px)"; card.style.opacity = (10 - index) / 10; }); if (newCards.length == 0) { $(".no-user").addClass("is-active"); } } initCards(); allCards.forEach(function (el) { let hammertime = new Hammer(el); hammertime.on("pan", function (event) { if (event.deltaX === 0) return; if (event.center.x === 0 && event.center.y === 0) return; el.classList.add("moving"); swipeContainer.classList.toggle("swipe_like", event.deltaX > 0); swipeContainer.classList.toggle("swipe_dislike", event.deltaX < 0); let xMulti = event.deltaX * 0.03; let yMulti = event.deltaY / 80; let rotate = xMulti * yMulti; event.target.style.transform = "translate(" + event.deltaX + "px, " + event.deltaY + "px) rotate(" + rotate + "deg)"; }); hammertime.on("panend", function (event) { el.classList.remove("moving"); swipeContainer.classList.remove("swipe_like"); swipeContainer.classList.remove("swipe_dislike"); let moveOutWidth = document.body.clientWidth; let keep = Math.abs(event.deltaX) < 200; event.target.classList.toggle("removed", !keep); if (keep) { event.target.style.transform = ""; } else { let endX = Math.max(Math.abs(event.velocityX) * moveOutWidth, moveOutWidth) + 100; let toX = event.deltaX > 0 ? endX : -endX; let endY = Math.abs(event.velocityY) * moveOutWidth; let toY = event.deltaY > 0 ? endY : -endY; let xMulti = event.deltaX * 0.03; let yMulti = event.deltaY / 80; let rotate = xMulti * yMulti; event.target.style.transform = "translate(" + toX + "px, " + (toY + event.deltaY) + "px) rotate(" + rotate + "deg)"; initCards(); } }); }); function createButtonListener(reaction) { let cards = document.querySelectorAll(".swipe--card:not(.removed)"); if (!cards.length) return false; let moveOutWidth = document.body.clientWidth * 2; let card = cards[0]; card.classList.add("removed"); if (reaction == "like") { card.style.transform = "translate(" + moveOutWidth + "px, -100px) rotate(-30deg)"; } else { card.style.transform = "translate(-" + moveOutWidth + "px, -100px) rotate(30deg)"; } initCards(); } $("#like").on("click", function () { createButtonListener("like"); }); $("#dislike").on("click", function () { createButtonListener("dislike"); }); }); } index.html.erb 今回はindex.html.erbに実装しています。 users.index.html.erb <div class="user-index-page"> <div class="swipe"> <div class="swipe--status"> <i class="fa fa-times"></i> <i class="fa fa-heart"></i> </div> <div class="swipe--cards"> <% @users.each do |user| %> <div class="swipe--card" id="<%= user.id %>"> <% if user.profile_image.url.nil? %> <div class="profile-default-img"></div> <% else %> <%= image_tag user.profile_image.url, class: "profile-img" %> <% end %> <p class="profile-name"> <%= user.name %> </p> </div> <% end %> <div class="no-user">近くにお相手がいません</div> </div> <div class="swipe--buttons"> <button id="dislike"><i class="fas fa-times fa-2x"></i></button> <button id="like"><i class="fas fa-heart fa-2x"></i></button> </div> </div> </div> これで完成です。 Railsのバージョンが違うと動かなくなるのでその都度確認してから実装してください。 後は適当にスタイルつけたら完成です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

中間テーブルのアソシエーション

概要 Railsにて、単語帳アプリを作成しました。 ログイン機能を有しており、Userモデルと、登録される単語をWordモデル、お気に入り機能もあるため、Favoriteモデルが存在します。 FavoriteモデルをUserモデルとWordモデルの中間テーブルとし、それぞれのアソシエーションを組もうとした際に調べたことを、備忘録としてまとめました。 アソシエーションを考える アソシエーションとは、モデルを利用したテーブル同士の関連付けのことで、これをモデルに定義することで、そのモデルに紐づく別のモデルの情報へアクセスできるようになります。 ではまず、UserとWordの関係性について考えます。 1人のユーザーは複数の単語を登録でき、反対に、単語は1人のユーザーに所属します。 この場合、ユーザーと単語の間には「一対多」の関係が成り立ち、以下のようなアソシエーションを組むことができます。 app/model/user.rb class User < ApplicationRecord has_many :words end app/model/word.rb class Word < ApplicationRecord belongs_to :user end アソシエーションを定義するとできること ここで、アソシエーションを組むことによって、何ができるかを確認します。 例えば、ユーザーのマイページがあって、そこに自分が登録した単語を一覧表示させたい場合、コントローラーでは、そのユーザーが登録した単語情報を取得する必要があります。 もしアソシエーションを組まずに、ある特定のユーザーの登録した単語の情報を取得しようとすると、次のような記述になります。(今回はusersコントローラーのshowアクション内に記述します) app/controllers/users_controller.rb class UsersController < ApplicationController def show @user = User.find(1) # usersテーブルからidが1のレコードを取得して、@userに代入 @words = Word.where(user_id: @user.id) # wordsテーブルからuser_idが@userのidと一致する全てのレコードを取得して@wordsに代入 end end    続いてアソシエーションを組んだ場合、以下のように記述することができます。 app/controllers/users_controller.rb class UsersController < ApplicationController def show @user = User.find(1) @words = @user.words end end 先程Wordモデルで定義した、has_many :wordsの:wordsの部分が、@user.wordsのような形で、Userクラス(モデル)のインスタンスメソッドとして使用できるようになった、という感じです。 このように、アソシエーションを組めばコードを簡潔に、直感的に記述することができます。 中間テーブルのアソシエーション 次に、お気に入りを含めたアソシエーションについて考えていきます。 ユーザーは複数の単語をお気に入りすることができます。反対に、単語は複数のユーザーからお気に入りされる可能性があります。つまり、ここではユーザーと単語の間に「多対多」の関係性が存在します。 中間テーブルは、このような多対多の関係にある2つのテーブル間の組み合わせだけをレコードとして保存する役割を持ち、「多対多」の関係を定義します。今回は中間テーブルとして、Favoriteモデルを用意し、お気に入り機能を実装します。 WordモデルとFavoritesモデル ではまず、Wordモデル、Favoriteモデルでのアソシエーションの定義の仕方を確認します。 app/model/word.rb class Word < ApplicationRecord belongs_to :user has_many :favorites has_many :users, through: :favorites end app/model/favorite.rb class Favorite < ApplicationRecord belongs_to :word end Wordモデルでは、先程定義したbelongs_to :userに加え、新たに次の2つを定義しました。 まず、1つの単語は複数お気に入りされる可能性があるため、has_many :favoritesを記述します。 続いて、has_many :users, through: :favoritesについてですが、has_manyメソッドのthroughオプションは「〜を経由する」という意味で、モデルに多対多の関連を定義するときに利用します。 上記のhas_many :users, through: :favoritesは、「単語は、お気に入りを介して複数のユーザーを抱えている」といった意味合いです。 例えば、 @word = Word.find(1) # 単語1 @users = @word.users とすることで、単語1をお気に入りしているユーザーの情報を取得できます。 またFavoriteモデルでは、お気に入りは単語に属するので、belongs_to :wordを定義しています。 UserモデルとFavoriteモデル 次に、UserモデルとFavoriteモデルでの定義の仕方です。 Favoriteモデルは先ほどと同じように、belongs_toを定義します。 app/model/favorite.rb class Favorite < ApplicationRecord belongs_to :user end 一方、Userモデルにおいて、先程のWordモデルでの書き方とほぼ一緒ですが、少し工夫が必要になります。 ポイントは、既にhas_many :wordsが定義されているという点です。 例えば以下のような記述をした場合、 app/model/user.rb class User < ApplicationRecord has_many :words has_many :favorites has_many :words, through: :favorites end 2行目と4行目でhas_many :wordsが被ってしまっています。 これだと、2行目のhas_many :wordsが4行目のhas_many :words, through: :favoritesによって上書きされることになります。 @user = User.find(1) # ユーザー1 @words = @user.words 上記のような記述をしたとき、本来であれば「ユーザー1が登録した単語の情報を取得」したかったところが、アソシエーションが上書きされた状態では、「ユーザー1がお気に入りした単語の情報を取得する」になってしまいます。 こうならないために、User.rbを以下のよう書き直します。 app/model/user.rb class User < ApplicationRecord has_many :words has_many :favorites has_many :fav_words, through: :favorites, source: :word end has_many :fav_words, through: :favorites, source: :wordについて、 has_manyの後には、:fav_wordsという仮の名前を定義してあげて、 through: :favoritesで、favoritesテーブルを経由して、 source: :wordのように、sourceの後に参照元になるモデルを指定してあげる といった具合で、アソシエーションを組んでいます。 このように:fav_wordsと定義することで、インスタンス.fav_wordsのようにメソッド化して使うことができます。 Railsは、アソシエーションを機能させる際、外部キーの名前などでテーブルの判断をしているそうですが、関連付け名(has_many :wordsの:wordsの部分のこと)を外部キーの名前から外れた名前にする場合(今回の例で言うと、has_many :wordsではなく、has_many :fav_wordsと定義すること)は、sourceオプションに関連付け元の名前を指定する必要があります。 これらのアソシエーションが組まれた上で、以下のように記述をすると、それぞれの単語情報を取得することができます。 @user = User.find(1) # ユーザー1 @words_1 = @user.words # ユーザー1の登録した単語を取得 @words_2 = @user.fav_words # ユーザー1のお気に入りした単語を取得 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

中間テーブルを用いた処理(Rails)

中間テーブルとは 中間テーブルは多対多の関係を表現するためのテーブルのことです。 テーブル同士が複数のレコードと対応している際に、中間テーブルを用いなければ非常に冗長なテーブル設計となってしまいます。 これだけでは意味不明だと思うので、下記の実例もよければ参考にしてください。 テーブル設計 今回はgroupsテーブルとusersテーブルを作成します。 グループには複数のユーザーが所属し、ユーザーは複数のグループに所属できる、という仕様にするためgroupsテーブルとusersテーブルは多対多の関係になります。 この関係を整理するために、ユーザーが所属するグループと、グループに所属しているユーザーを管理する中間テーブル(group_usersテーブル)を作成します。 中間テーブルを作らなかったら? usersテーブルでユーザーが所属するグループを管理、もしくはgroupsテーブルでグループに所属しているユーザーを管理する事になります。 下記の例では、usersテーブルでユーザーが所属するグループを管理しています。 これではユーザーが新しいグループに参加するたびに、usersテーブルのカラムを増やす事になるため、テーブル設計時にカラム数を決めることができません。 カラムを増やさないために以下のようなテーブル設計にすることもできますが、これではusersテーブルに同じユーザーが2人以上存在する事になってしまいます。 実装編 今回はRailsを用いて、グループにユーザーを追加する処理を実装してみます。 モデルの実装内容は以下の通り。 ・Userモデル class User < ApplicationRecord has_many :group_users has_many :groups, through: :group_users end ・Groupモデル class Group < ApplicationRecord has_many :group_users has_many :users, through: :group_users end ・GroupUserモデル class GroupUser < ApplicationRecord belongs_to :user belongs_to :group end 画面の動きとしては、以下のGif画像のように登録ボタンを押下したらgroupに登録される感じです。 コントローラー側ではadd_userというユーザー追加用のメソッドを定義しています。 このメソッドの4行目でリレーションを追加する事により、中間テーブルにグループとユーザーが追加されます。 def add_user @group = Group.find(params[:group_id]) user = User.find(params[:user_id]) @group.users << user redirect_to group_path, notice: "ユーザーを追加しました。" end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング学習に役立つサイト一覧

HTMLクイックリファレンス フロントエンド実装はとりあえずこのサイトに目を通しておけばOK。早見表なので詳細な内容ではなく、要素毎に内容が詰め込まれている。 Pikawaka - ピカ1わかりやすいプログラミング用語サイト Ruby on Railsの学習でかなりお世話になっているサイト。内容が豊富で、完璧主義(笑)な自分の知りたいことも結構記載されていて助かってる。 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 略して、わわわIT用語辞典!。ゆる〜いキャラクター達がゆるゆるとIT用語を解説している。 他にもあったら、どんどん追記すべし!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js + Rails + Firebase Authでjwt認証を実装しよう(Email編)

はじめに JWT認証に関して、Next.js Rails Firebase Authenticationという組み合わせでの実装をした記事があまりないとのことなので、今回は実際にやっていこうと思う。今回はEmailでの実装になるため、Google認証で実装をしたいという方は同シリーズの https://qiita.com/satopin/items/fa0c35a0ba69a379683e をご覧いただければ幸いである。 ソースコードはこちらから https://github.com/yumaasato/my-mahjong (まだアプリ自体は作成中なので悪しからず・・・) 環境 Ruby: 2.6.5 Rails 6.0.3.6 TypeScript firebase-auth-rails Redis-server *ちなみに今回、user認証でよく用いられるdeviseは必須ではありません。 今回は使わずに実装を進めていきます。 また、今回redis-serverのインストールからconfig/initializers/firebase_id_token.rbで設定を行うまでは https://qiita.com/satopin/items/fa0c35a0ba69a379683e  と同じのため、3-1までは飛ばしてもらって構わない。 1. Redis-serverのインストール mac版の場合 $ brew install redis-server Linuxでは $ sudo apt install redis-server 内部でfirebase_id_tokenを使っているためこのRedisをインストールする必要があるとのことです。 2. firebase-auth-railsの追加 今回、firebase-auth-railsというgemを利用することでjwt認証を比較的簡単に行うことができます。 Gemfile gem 'firebase-auth-rails' を追加します。 そして、 $ bundle install を実行します。 3-1. 実装(プロジェクトの設定) まずはじめに、Reidsとfirebaseプロジェクトの設定をおこないます。 config/initializers/firebase_id_token.rb FirebaseIdToken.configure do |config| config.redis = Redis.new config.project_ids = [ENV['FIREBASE_PROJECT_ID']] end 3-2.Userテーブルの設計 今回は、Userのnameをクライアント側で登録せずにEmailとPasswordのみの認証を行う。 そのため、Userテーブルの編集を行う必要がある。 以下は一例である。 db/migrate/****_create_users.rb class CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :password_digest t.string :name, default: "" # null: falseとnameが必須になるので今回は外す t.string :email, default: "" t.string :uid, null: false, default: "" t.timestamps t.index :uid, unique: true # 同じuidによる登録を防ぐ end end end ここで、uidを null:falseにしておくことで、Firebase Authで認証したUserをRails側で受け取れるようにしておく。 3-3.ユーザー登録用のコントローラを実装する 以下はあくまで一例である app/controllers/api/v1/auth/users_controller.rb require_dependency 'api/v1/application_controller' module Api module V1 module Auth class UsersController < V1::ApplicationController skip_before_action :authenticate_user def index # ユーザー一覧を取り出す users = User.order(created_at: :desc) render json: { status: 'SUCCESS', message: 'Loaded users', data: users } end def create # ユーザーを作成する FirebaseIdToken::Certificates.request raise ArgumentError, 'BadRequest Parameter' if payload.blank? @user = User.find_or_initialize_by(uid: payload['sub']) do |user| user.email = payload['email'] end if @user.save render json: @user, status: :ok else render json: @user.errors, status: :unprocessable_entity end end private def token params[:token] || token_from_request_headers end def payload @payload ||= FirebaseIdToken::Signature.verify token end end end end end Google認証では、 @user = User.find_or_initialize_by(uid: payload['sub']) do |user| user.name = payload['name'] end としたが、これでは、payloadでUserのnameが要求される。今回Userのnameは使わずに、Emailのみの認証を行うため、 @user = User.find_or_initialize_by(uid: payload['sub']) do |user| user.email = payload['email'] end とする。ちなみに user.password= payload['password']とする必要はない。Passwordを暗号化した文字列ならわかるが、データベースにpasswordを保管するのはセキュリティ的に問題があるからだ。 3-4 Routeの追加 Rails.application.routes.draw do namespace 'api' do namespace 'v1' do resources :players resources :games, only: %i(index) namespace 'auth' do post 'users' => 'users#create' #追記箇所 get 'users' => 'users#index' #追記箇所 end end end end 今回の場合、Firebase Authを介してUserの認証を行なっているため、上記のようにする。 ちなみに、get 'users' => 'users#index'は、User一覧をみれるようにするために設定した。 4-1. クライアント側の実装 次にNext.js側の実装を行う。フロント側の実装方法が省かれている記事が多いが、ここではNext側についても実装方法を記述する。あくまで参考(一部省略)だが、 pages/sign_in/Auth.tsx // Eメール認証 const signUpEmail = async () => { await auth.createUserWithEmailAndPassword(email, password).catch((err) => alert(err.message)); router.push('/') }; // 認証後Rails側にリクエストを送る const handleEmailsignUp = () => { const request = async () => { await signUpEmail(); const auth = getAuth(); const currentUser = auth.currentUser; // Firebase Authの認証 if (auth && auth.currentUser) { const token = await currentUser.getIdToken(true); const config = { token }; // Rails側にリクエストを送る try { await axios.post('/api/v1/auth/users', config); } catch (error) { console.log(error); } } }; request(); }; return ( <> <TextField variant="outlined" margin="normal" required fullWidth id="email" label="Email Address" name="email" autoComplete="email" value={email} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setEmail(e.target.value); }} /> <TextField variant="outlined" margin="normal" required fullWidth name="password" label="Password" type="password" id="password" autoComplete="current-password" value={password} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setPassword(e.target.value); }} /> <Button disabled={ isLogin ? !email || password.length < 6 : !email || password.length < 6 } fullWidth variant="contained" color="primary" className={classes.submit} startIcon={<EmailIcon />} onClick={ isLogin ? async () => { try { await handleEmailLogin(); } catch (err) { alert(err.message); } } : async () => { try { await handleEmailsignUp(); } catch (err) { alert(err.message); } } } > {isLogin ? "Login" : "Register"} </Button> </> ) これでButtononClick 5-1. 実装後のオペレーション(重要) 実装後、$ rails cとして、firebase_id_tokenのDownloading Certificates以下に記載してある内容を実行する必要がある。 irb(main):001:0>FirebaseIdToken::Certificates.request ・ ・ ・ irb(main):002:0>FirebaseIdToken::Certificates.present? => true となってはじめて、jwt認証を使うことができる。 注意点 この実装ではnameを使わずに実装をおこなったため、クライアント側でnameを入れて認証を行なった際には、エラーが起こる可能性がある。nameを入れて実装を進めたいという場合には、別の方法を参照してもらいたい。 また、Railsにおいて、.envファイルで環境変数を管理する際には、 gem 'dotenv-rails' をインストールする必要があるため、忘れずに設定しておこう。 (これで認証に時間がかかったので・・・) さらに、認証を行う際には、Railsサーバーとクライアント側のサーバーに加えて、$ redis-serverでRedis-serverを起動する必要があるため、忘れないようにしよう。 終わりに 実装だけでもかなりの時間をかけた上に、Next.js(TypeScript) + Rails + Firebase Authの組み合わせでのjwt認証の記事が少ないということで今回記事を書くことになった。この記事が一人でも多くの人の参考になれば幸いである。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsアプリケーションを途中からReactのSPAに書き換える際に設定したこと

この記事の内容 既存Railsアプリを徐々にSPAに置き換えていく、という課題を研修でやりました。 Reactの設定ファイルは先輩が書いてくださったので、その内容を読み解きながら、RailsアプリにReactを導入していく流れを確認しようと思います。 なお、実行環境は以下の通りです。 Rails 6.0.3 React 17.0.2 Rails6系なのでWebpackerはデフォルトで入っているものとします。 ディレクトリ構成 今回の(最終的な)ディレクトリ構成は下記の通りです。関連のあるところだけ記載しています。なお、ここに記載のファイル以外に、config/routes.rbも編集しています。 . ├── controllers │   └── spa │      └── spa_roots_controller.rb └── javascript    ├── App.jsx    ├── Routes.jsx    ├── components    │   └── Bar.jsx    ├── packs    │   ├── application.js    │   ├── root_application.jsx    │   └── server_rendering.js    └── pages    └── Foo.jsx ゴール画面 今回のゴール像は以下のイメージです。 Fooページの中にBarコンポーネントを表示したいと思います。 Reactをインストール まず、gem react-railsをインストールします。 Gemgile gem 'react-rails' このGemを利用する以外の方法も調べはしたのですが、今回はコードの読み解きに留めることにしました。 参考:ReactをRailsと共に使う方法 なお、このgemは先に紹介した記事によると、「RailsのAsset Pipelineを利用してJSXをRailsが認識できる形に処理し」てくれるそうです。 公式サイトの記載通りに、コマンドを実行します。 $ bundle install $ rails webpacker:install $ rails webpacker:install:react $ rails generate react:install その結果、以下のファイルが生成・追記されます。 app/javascript/components/ # Reactのコンポーネント用に生成 app/javascript/packs/application.js # ReactRailsUJSの記載が追記 app/javascript/packs/server_rendering.js # サーバーサイドのレンダリング用に生成 ここで、application.jsとserver_rendering.jsには同じ内容が記載されていましたが...。 var componentRequireContext = require.context("components", true); var ReactRailsUJS = require("react_ujs"); ReactRailsUJS.useContext(componentRequireContext); require.context("components", true);は、/componentディレクトリ下の全てのファイルを拾うという設定、useContext(設定)は、設定に書かれた要素がグローバルな要素として使われるという記載のようです。 それぞれ、以下を参考にしました。 Webpackでフォルダ内の全ファイルを一気にrequireする React hooksを基礎から理解する (useContext編) RailsのルーティングとControllerの編集 次にRails側の、設定ファイルを書いていきます。 まずはconfig/routes.rbに下記のように記載します。 config/routes.rb scope 'spa' do get '*path', to: 'spa_roots#show' end こちらの記述によって、/spa/*で始まるリクエストが来たらなんでも、spa_rootsコンロトーラーのshowアクションに飛ぶようにします。 (namespaceではなくてscopeである理由は、先輩が参考記事を貼ってくれていました。) Railsのroutingにおけるscope / namespace / module の違い そして、spa_routes_controllerには、ほぼ何も書かず...。 app/controllers/spa_roots_controller.rb class SpaRootsController < ApplicationController def show; end end 代わりに、spa_roots#showのViewの方にエントリーポイントとなるファイルを読み込む記載を入れます。 app/views/spa/spa_roots/show.slim = javascript_pack_tag 'root_application' この時読み込んでいるroot_applicationには何が書かれているかというと、以下のとおりです。 app/javascript/packs/root_application.jsx import { App } from '../App' import React from 'react' import ReactDOM from 'react-dom' document.addEventListener('DOMContentLoaded', () => { ReactDOM.render( <App />, document.body.appendChild(document.createElement('div')) ) }) ReactDOM.renderは以下のような形で使われ、React公式サイトによると、 ReactDOM.render(element, container[, callback]) 渡された container の DOM に React 要素をレンダーし、コンポーネントへの参照(ステートレスコンポーネントの場合は null)を返します。 とのことですので、 document.body.appendChild(document.createElement('div') の部分で、bodyの末尾に作られた<div>タグに<App />をレンダーさせます。 参照:ReactDOM#リファレンス Routerの導入 では、先程記載したこの<APP />には何が書かれているのかというと、以下の内容です。 app/javascript/App.jsx import React from 'react' import { BrowserRouter as Router } from 'react-router-dom' import { Routes } from './Routes' export const App = () => { return ( <Router> <Routes /> </Router> ) } Routerが出てきましたね。実は、順番が前後してしまいましたが、この少し前にreact-router-domをインストールしました。 yarn add react-router-dom そして、app/javascript/App.jsxではBrowserRouter(Routerという名前で使用)とRoutesというライブラリを読み込んでいます。 それぞれのライブラリに関して深入りはしませんが、 BrowserRouter ... ルーティングの切り替え機能を提供 Routes ... それぞれのルーティングを提供 しているようです。 参考:【React】ルーティング設定方法 試しにページを表示 そして、この<Routes />には次のように書いてありました。<Switch />を使って、リクエストごとに表示する内容を切り替えていきます。 APIドキュメント:Switch app/javascript/Routes.jsx import React from 'react' import { Switch, Route } from 'react-router-dom' import { Foo } from '~/pages/Foo' export const Routes = () => { return ( <Switch> <Route path="/spa/foo"> <Foo /> </Route> </Switch > ) } ここまでで、Fooコンポーネントは表示されます。 この、Fooコンポーネント内で、さらにBarコンポーネントを呼び出せば、完成です。 app/javascript/pages/Foo.jsx import React from 'react' import { Bar } from '~/components/Bar' export const Foo = () => { return ( <div> <h1>これはfooページです。</h1> <Bar /> </div> ) } 完成! 以上で、RailsにReactを途中から導入する設定が完了です。 いやー、、、先輩いなかったら完全に彷徨っていたと思います。。。 まだまだJSはわからないことが多いので、これからも地道に調べながら理解を深めていきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む