20210419のRubyに関する記事は19件です。

【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で続きを読む

デフォルト画像を設定する方法

はじめに 画像が登録されてない場合のデフォルト画像を設置する方法についてまとめていきます。 今回は、CarrierWave、asset pipelineを使用したやり方になります。 後半では、The asset "" is not present in the asset pipelineのエラーについて少し触れていきます。 CarrierWaveでデフォルト画像を定義 [app/uploaders/image_uploder.rb] このように前もってデフォルト画像を設置するためのメソッドがコメントアウトして用意されているので、必要な部分を解除する。 # def default_url(*args) # # For Rails 3.1+ asset pipeline compatibility: # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.jpeg"].compact.join('_')) # # "/images/fallback/" + [version_name, "default.png"].compact.join('_') # end 解除する場所。 def default_url(*args) ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.jpeg"].compact.join('_')) end "default.jpeg"の部分が、デフォルト画像のパスになります。 なので、デフォルトの画像のパスを統一してあげる作業が必要になります。 デフォルト画像の用意 実際に使用するデフォルトの画像をapp/assets/images/の配下に保存します。 先ほどもいったように保存するときは、"default.jpeg"の名前、形式を統一する必要があります。 ビューで呼び出し <%= image_tag '/assets/default.jpeg',:alt => 'ユーザーアイコン' %> 'default.jpeg'でもok 注意点としては、 ・/assets/images/...とimagesを指定しないこと。 imagesを使用すると以下のような表示になる。 また、logではエラーが吐かれている。 ActionController::RoutingError (No route matches [GET] "/assets/images/default.jpeg"): まとめ 簡単ではありますが、デフォルト画像の設置方法についてまとめてみました。 少しでも参考になれば嬉しいです。 補足(The asset "" is not present in the asset pipelineエラーについて) アセットパイプラインが存在しないと出てしまう原因。 結論 config/envitonments/production.rbファイルの config.assets.compile = true falseをtrueにしてあげる アセットアイプラインへのアクセスを許可するみたいなイメージだと思います。 参考資料 CarrierWaveでデフォルト画像の設定 アセットパイプライン、img_tagの使い方について記載されています。 Railsガイド The asset "" is not present in the asset pipeline GitHub/carrierwave アセット使用しない方法も記載されています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】クラスとインスタンスについて(メモ)

クラスとインスタンス クラス・・・あるものを作る設計図のようなもの インスタンス・・・クラスから生成されるもの(オブジェクト) (具体例) ①クラス⇨車  インスタンス⇨ハンドル、ブレーキ、タイヤ、ライトなど        (またはスポーツカー、救急車、トラックなど) ②クラス⇨家  インスタンス⇨玄関、キッチン、洗面所、階段など インスタンス作成 ①Carクラスを作成 ②initializeメソッドと種類を表示するtypeメソッドをクラス内に作成 ③インスタンス(sports_car、ambulance、track)作成 ④インスタンスに情報(メソッド)を与える。 以下、作成例 class Car def initialize(name) @name = name end def type puts "私は#{@name}が好きです" end end sports_car = Car.new("スポーツカー") ambulance = Car.new("救急車") track = Car.new("トラック") sports_car.type ambulance.type track.type #出力↓ #私はスポーツカーが好きです #私は救急車が好きです #私はトラックが好きです 以上
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

sketchupでruby その6

概要 sketchupでrubyやってみた。 material使ってみた。 写真 サンプルコード def colorbox model = Sketchup.active_model entities = model.active_entities Sketchup.set_status_text "Please wait a little." for x in 0..8 do for y in 0..8 do for z in 0..8 do group = entities.add_group entitie = group.entities pts = [] pts[0] = [x * 10 + 5, y * 10 + 5, z * 10 + 5] pts[1] = [x * 10 + 10, y * 10 + 5, z * 10 + 5] pts[2] = [x * 10 + 10, y * 10 + 10, z * 10 + 5] pts[3] = [x * 10 + 5, y * 10 + 10, z * 10 + 5] base = entitie.add_face pts base.pushpull 5 materials = model.materials m = materials.add "ohi" + String(x) + String(y) + String(z) m.color = Sketchup::Color.new(x * 32 , y * 32 , z * 32) m.alpha = 0.7 entitie.each { |e| case e when Sketchup::Face e.material = "ohi" + String(x) + String(y) + String(z) end } end end end end 以上。
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

sketchupでruby その5

概要 sketchupでrubyやってみた。 followme使ってみた。 写真 サンプルコード def sphere model = Sketchup.active_model entities = model.active_entities x = 0 y = 0 z = 0 r = 30 n = 24 circleBase1 = entities.add_circle(Geom::Point3d.new(x, y, z), Z_AXIS, r, n) circleBase2 = entities.add_circle(Geom::Point3d.new(x, y, z), X_AXIS, r, n) base = entities.add_face(circleBase1) status = base.followme(circleBase2) end 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sketchupでruby その4

概要 sketchupでrubyやってみた。 pushpull使ってみた。 写真 サンプルコード def cube cube = Sketchup.active_model.definitions.add("Cube"); unit = 10 p = Array.new; p[0] = ORIGIN; p[1] = [unit, 0, 0]; p[2] = [unit, unit, 0]; p[3] = [0, unit, 0]; face = cube.entities.add_face(p); face.reverse! if face.normal.z < 0; face.pushpull(unit); trans = Geom::Transformation.new([5, 5, 5]); Sketchup.active_model.active_entities.add_instance(cube, trans); end 以上。
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

複数の値を標準入力から受け取る方法

はじめに paizaのスキルチェックは、一度に複数の値が入力される問題だった。 一度に複数の値を受け取る方法が分からなかったので備忘録として残しています。 結論 splitメソッドを利用する事で複数の値を受け取ることができます。 apple orange grape 例えば、上記の3つが標準入力される値とした場合、下記のように記述する事で fruits = gets.split(' ') p fruits まとめて値を受け取ることができます。 ※受け取った値は配列となっているので、数値を使用する場合は文字列から数値に変換する必要があります。 ["apple", "orange", "grape"]
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

vscodeのemacs的なtune-up

!macOS-11.2.3 !ruby-2.7.2p137 状況 私のcoding環境はshell + emacs ですが,学生さんにそれを教えるとだいぶハードルが高いみたいです.なら,教える側が歩み寄ればいいんですが,この一年四苦八苦したのですがvscodeを使えませんでした.その様子をまとめると次の通りです. まず,教えるときにはmacとwin10の両方を使う必要があります.ずっとmacを私は使っていて,学生さんたちはwin10を買わされているからです.macを使っているとcommand系のkeyboard shortcut(OSXへの命令)とctrl系のshortcut(emacsへの命令)をうまく頭の中で切り替えることができます. ところがwin10だとこれが難しくなります.vscodeのextensionのawesome emacs keybindを使うとほぼ完全にvscodeのemacs化ができますが,これではハードルの高いemacsを教えているのと同じになります.こうして,最初の一年はvscodeとの違いが,emacsで頻出するブロック選択とかbuffer移動などたくさんあり,不便だなーーと思っていました.一方で,vscodeのlintのsuggestionやterminal, folder同時表示はcoding初心者にはとても直感で魅力的です. 色々vscodeのtune upをするうちに,カーソル移動だけが置き換えることができないということがわかりました.違いを融合して,年取って物覚えが悪くなったEmacs老人が,初心者に優しいvscode使いになれる環境構築をまとめてみました. ruby on vscode 最初の一歩は小さく...です.target言語はRuby,あるいはそのエコシステムです.rubyはpythonみたくterminalで動かせるいいextension runnerがありません.code runnerはoutputへ持っていく(これは後で修正します)ので違和感が大きかったです.そこで,ruby runnerである程度動かせるようにしました.ctrl-shift-pで最近のコマンドを選べばストレスを貯めずにしばらくは動かせます. code runner いじる ちょっと高度な技となりますが,code runnerのpackage.jsonを直接編集します. https://murabitoleg.com/vscode-runner/ ~/.vscode/extensions/formulahendry.code-runner-0.11.3/package.json "code-runner.runInTerminal": { "type": "boolean", "default": true, ... false -> true 簡単なcode(hello worldとか)ならこれで,いい感じ!!! terminalとの行き来(c-x, c-s, c-z, fg) terminalとの行き来ができるようにします.これは,shellによるargsの取得とか,redirectionで色々操作するため. on mac: ctrl-`でterminalとeditorを行ったり来たり(バッククォート( ` )はJIS配列の場合 Shift + @ で入力可) on win10: だめ.short-cutを新たに登録.ctrl-k, ctrl-sでショートカットの編集画面.そこで,terminal.focusと打ち込んで,workbench.action.terminal.focusを選んで,編集アイコンを選ぶ.その後,ctrl-1とした.どっかとぶつかっているが気にせずにそのままreturnしたら,これで行ったり来たりができる. Visual Studio Code でエディタとターミナルを移動するショートカットキーの作成 結局,emacsからvscodeへの頭の切り替えは,これが決め手.editorの問題ではなしに,shellとの連携だったようです.問題の場所の特定を間違っていた.やれやれ. おまけ 以降は本筋から外れますが,いくつかのお手軽チューンナップのメモです. font shellがfishだと文字化けが気になります. code -> preferences -> settings(command-,)を開ける terminal integrated font familyを検索 Meslo LG M DZ for Powerline win10も綺麗に表示してくれますね. terminalのkeybind 結局ここ.shellでctrl-p,n,b,f,k,yが使えない(except d)...でも,editorで慣れてくるとこれもover writeできるかも.ubuntu on wslはこのkeybindが使えるので,この統一感のなさは仕方がないのかな...win10もkeyboard替えればいいのに. 解決策はあります editor画面の切り替え(c-x o) Emacsで頻出するbuffer移動の代わりです. command-option-left arrow とかright arrow これとcommand-shift-pを組み合わせる formatter Ruby用のformatterです. option-shift-fです. rufo ruby formatter gem install rufoがあらかじめ必要 source ~/git_hub/grad_research_21s/docs/orgs/c02_vscode_setup.org
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

ActiveHashを導入した際のselectメソッドの使用方法

環境 'rails', '~> 6.0.0' 'capybara', '>= 2.15' ruby '2.6.5' はじめに systemspecによる結合テストを行なっている際に、トップページレンダーした時に、以下のようなエラーが発生しました。 結論からお話しますと、このエラーはCapybaraのselectメソッドの記述ミスにより発生するものでした。 以下、エラーが発生した記述ではselectメソッドで’法規・条例’という文字をcategoryから探して選択するという記述をしているのですが、 実際の挙動としてはそこで選択した項目のidを引数として、トップページに渡すという作業をしています。 undefined method `[]' for nil:NilClass とはselectメソッドで指定すべきidがありませんという内容と想定されます。 そのため、selectメソッドの中に引数となるidの番号を含めて記述しなくてはなりません。 エラーの原因となったコードを見てみますと、 select '法規・条例', from: 'tip_category_id' となっておりますが、これをidを含めた形に修正すると、以下のようになります。 select Category.data[@tip.category_id - 1][:name], from: 'tip_category_id Categoryモデルから@tipを引数にidを選択する記述に変更しました。 なお、activeHashは0から始まる番号で数字を選択するため、idとの関連性を考慮し、category_id -1 をしております。 以下、記述内容 エラー本文 Failure/Error: <p class="tiped-title-contents">カテゴリー : <%= Category.data[f.category_id][:name] %></p> ActionView::Template::Error: undefined method `[]' for nil:NilClass [Screenshot]: /Users/taniguroarata/originalapp/archtips/tmp/screenshots/failures_r_spec_example_groups_nested_nested_visit_new_user_session_path_231.png # ./app/views/tips/_main_area.html.erb:17:in `block in _app_views_tips__main_area_html_erb__1894181271776908624_70358077282820' # ./app/views/tips/_main_area.html.erb:12:in `_app_views_tips__main_area_html_erb__1894181271776908624_70358077282820' # ./app/views/tips/index.html.erb:10:in `_app_views_tips_index_html_erb___4391342711310590138_70357992921920' # ------------------ # --- Caused by: --- # NoMethodError: # undefined method `[]' for nil:NilClass # ./app/views/tips/_main_area.html.erb:17:in `block in _app_views_tips__main_area_html_erb__1894181271776908624_70358077282820' spec/system/tips_spec.rb require 'rails_helper' RSpec.describe "投稿する", type: :system do before do @tip = FactoryBot.create(:tip) @user = FactoryBot.create(:user) end context '投稿に成功した時' do it 'visit new_user_session_path' do visit new_user_session_path fill_in 'user_email', with: @user.email fill_in 'user_password', with: @user.password find('input[type="submit"]').click expect(current_path).to eq(root_path) expect(page).to have_content('新規投稿') visit new_tip_path fill_in 'tip_title', with: @tip.title select '法規・条例', from: 'tip_category_id' #ここがエラーの原因です #以下のように修正します select Category.data[@tip.category_id - 1][:name], from: 'tip_category_id fill_in 'tip_description', with: @tip.description binding.pry expect{ find('input[type="submit"]').click }.to change {Tip.count }.by(1) expect(current_path).to eq(root_path) end end end app/views/tips/new.html.erb (テスト時の画面) ※画像の投稿は必須ではないため、今回は飛ばしてテストしております <div class = "wrapper"> <%= render "shared/header" %> <div class="main-tips-new"> <div class = "new-tip-string"> <p>新規投稿</p> </div> <%= form_with(model: @tip, local: true) do |form| %> <%= render 'layouts/error_messages', model: form.object, class:"" %> <div class="tip-title"> <p>タイトル</p> <div class="tip-title-content"> <%= form.text_field :title, placeholder: "" %> </div> </div> <div class="tip-category"> <p>カテゴリー</p> <div class="tip-category-content"> <%= form.collection_select(:category_id, Category.all, :id, :name, {}, {class:""}) %> #ここにactiveHashを用いて、番号によって項目を、選択をしております。 </div> </div> <div class="tip-images"> <div class="tip-image-main"> <p class="tip-image-main-text">画像(メイン)</p> <div id="image-list"> <%= form.file_field :image, class: 'tip-image-main-img' %> </div> </div> <div class="tip-image-sub"> <p class="tip-image-sub-text">画像(サブ)</p> <div class="tip-image-sub-images"> <%= form.file_field :image, class: 'tip-image-sub-img' %> <%= form.file_field :image, class: 'tip-image-sub-img' %> <%= form.file_field :image, class: 'tip-image-sub-img' %> </div> </div> </div> <div class="tip-description"> <p class="tip-description-text">説明</p> <%= form.text_field :description, placeholder:"", class: 'text-form' %> </div> <%= form.submit "投稿する" %> <% end %> </div> </div> app/models/category.rb class Category < ActiveHash::Base self.data = [ { id: 1, name: '--' }, { id: 2, name: '法規・条例' }, { id: 3, name: '納まり・詳細図' }, { id: 4, name: '仕上げ' }, ] include ActiveHash::Associations has_many :categories end 終わりに select等の要素を選択する際にはbinding.pryで値を確認しつつ行うとうまくいくことが多いです。 この記事が皆様のお役に立てればとおもいます。
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む