- 投稿日:2020-09-27T22:38:57+09:00
Rails: rake taskを良い感じに書く方法
良い感じ = 以下の二点
- テストしやすいこと
- 工数削減のため
- (ある程度は)エンジニア間での書き方が統一できること
- 例えば
dryrun
の指定方法が書き手によってまちまちだと、商用でのtask実行時に事故が起きる可能性もあるので統一可能であればした方が良い- ログのフォーマットが統一されていないと作業効率が悪い
などなど
コード
lib/tasks/issue_6885.rbrequire_relative 'helpers/all_user_name_update_helper.rb' namespace :issue_6885 do desc 'これはサンプルです' task all_user_name_update: :environment do helper = AllUserNameUpdateHelper.new helper.main end endlib/tasks/helpers/all_user_name_update_helper.rbrequire_relative 'rake_helper_template' class AllUserNameUpdateHelper < RakeHelperTemplate NEW_NAME = 'bar' def main template do |logger| all_users = get_all_user logger.info("対象ユーザー数: #{all_users.size}") if all_users.blank? logger.info('対象ユーザー数が0件だったため処理を終了します') return end all_users.each { |user| update_name(user, NEW_NAME) } end end private def get_all_user User.all end def update_name(user, new_name) user.update!(name: new_name) user end endlib/tasks/helpers/rake_helper_template.rbclass RakeHelperTemplate # 標準出力も行う def make_logger(log_file_path) logger = ActiveSupport::Logger.new(log_file_path) stdout_logger = ActiveSupport::Logger.new(STDOUT) broadcast_logger = ActiveSupport::Logger.broadcast(stdout_logger) logger.extend(broadcast_logger) logger.formatter = Logger::Formatter.new logger end # ログファイル名はtask名と同じにする(コロンは使わない方が良いので置換する) # rakeタスクの実行以外から呼び出されるケース(例: spec)を考慮しておく def make_log_file_path task_name = Rake.try(:application)&.top_level_tasks&.[](0)&.gsub(':', '_') || Rails.env log_file_name = "#{task_name}.log" Rails.root.join('log', log_file_name) end def template log_file_path = make_log_file_path # 明示的に文字列のfalseを渡さない限りは必ずdryrunにする is_dryrun = ENV['is_dryrun'] != 'false' logger = make_logger(log_file_path) logger.info("Start. is_dryrun: #{is_dryrun}") ActiveRecord::Base.transaction do yield(logger) raise ActiveRecord::Rollback if is_dryrun end logger.info("Finish. log_file_path: #{log_file_path}") end end実行例
dryrunにする場合$ is_dryrun=false bundle exec rake issue_6885:all_user_name_update I, [2020-09-27T13:33:27.229764 #13040] INFO -- : Start. is_dryrun: true I, [2020-09-27T13:33:27.556890 #13040] INFO -- : 対象ユーザー数: 1 I, [2020-09-27T13:33:27.755933 #13040] INFO -- : Finish. log_file_path: /app/log/issue_6885_all_user_name_update.logポイント
- .rakeファイルはhelperクラスを呼び出すだけにして、rakeのDSL?的なお作法を気にせずに普段書き慣れているclassのメソッドをテストするようにした
- yieldを使って
dryrun
,logger
(の一部)を共通化し、rake taskを作成するたびに毎回定義しなくても済むようにした
- mainメソッド(名前は何でも良い)には極力そのtaskの処理フローだけを定義するようにし、個別具体的な処理はテストしやすい粒度で別メソッドに切り出すようにする
テンプレートにしてしまえばall_user_name_update_helper.rbのようにやりたいことに集中できる!
(^○^)
所感
- rake taskの書き方は久々に書くと忘れてるのでこの記事は実は備忘録的な意味合いもあったり
- yieldは正直わかりにくいので使いたくない派だけど、テンプレートのように毎回書いたり読んだりしないものであれば良いかなという考え
- 他にも良い感じの書き方あるよという方いたら教えてください!
- 投稿日:2020-09-27T21:29:02+09:00
【第1回】RSpecビギナーが、ビギナーなりにModelSpecを書いてみた
はじめに
はじめまして。最初に自己紹介を簡単にさせて頂きます。
2020年5月〜2020年9月までDMMWEBCAMPにてRubyを中心に学習し、現在転職活動中の卒業生です。
先日、RSpecの雄である伊藤淳一 @jnchito さんのご厚意で開催された初学者向けの勉強会(RSpecビギナーズ!!)にも参加致しました。ポートフォリオにRSpecを用いたテストを記述する中で感じた、
私と同じ初学者なら「ここでつまづくだろうな」とか「ここが分からん」といった"つまづきポイント"を稚拙ながら初学者目線でまとめました。
また、やっていく中で自分が書きたい機能の具体的なテストコード例が欲しいと思うことがあったので、自分の復習も兼ねてますが、この記事がRSpecビギナーズにとって少しでも参考になれば嬉しいなと思います。
※厚かましくはありますが、この記事をたまたま見た"RSpecエキスパート"がいらっしゃいましたら、下手くそなコードに是非アドバイスいただけると幸いです。この記事で扱うこと
- ModelSpec(モデルスペック)
アプリケーションのコア部分であるモデルのテスト- モデルスペックの具体的な記述例
自身のポートフォリオを参考にして記述していきますこの記事で扱わないこと
- SystemSpec(システムスペック)
モデル、コントローラ、ビュー、全部テストできるよ! 詳細はこちらをご覧下さい。- RSpecのセットアップ、準備
(後述する参考書籍『EverydayRails-RSpecによるRailsテスト入門』にて詳しく記載してあるのでそちらを参考にしてください)前提
- 対象
RSpec書こうとしてるけど何が何だかさっぱりなんじゃあ〜という初学者の方。 ただ、伊藤さんの使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 こちらの記事内容をある程度は見ていたり、なんかやったことはあるな〜とか、最低限describe,it,expectの役割が分かる方が望ましいです。参考コード
前述したように自身のポートフォリオを参考に記述するので、こちらにGitHubのリンクを貼っておきますが、テストの対象はサイトの基幹機能に絞っていますのでご了承ください。
【サイトの基幹機能】(個人・法人会員の新規登録/ログイン/編集、法人会員登録の申請、記事の投稿/編集、DM、通知など)
DM・通知などは次回SystemSpec編でご紹介します。テストを記述するための準備
RSpecによるテストを記述するためには、gem 'rspec-rails'をはじめ、いくつかgemを入れたり設定をする必要があります。
まずはテストを書く準備を整えてからお読みください。
必要なものは『EverydayRails-RSpecによるRailsテスト入門』に記載してあります。
というか、これを見ればこの記事を見なくても分かる人は分かると思います。
具体例としてコードを見たいという方はそのままお読みいただけると嬉しいです。(本当に参考程度ですが)個人ユーザーモデルのテスト
①個人ユーザーモデルの紹介
まず初めに個人ユーザーモデルをお見せします。
実際はfavoriteモデルなどのアソシエーションもありますが、今回は扱わないので敢えて削除しております。①user.rbclass User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :rooms, dependent: :destroy has_many :messages, dependent: :destroy has_many :notifications, dependent: :destroy validates :last_name, presence: true, length: { maximum: 10 } validates :first_name, presence: true, length: { maximum: 10 } validates :kana_last_name, presence: true, length: { maximum: 10 } validates :kana_first_name, presence: true, length: { maximum: 10 } validates :email, presence: true, length: { maximum: 30 } validates :postal_code, presence: true, length: { minimum: 7, maximum: 7 } validates :address, presence: true, length: { maximum: 30 } validates :phone_number, presence: true, length: { maximum: 12 } validates :introduction, length: { maximum: 200 } attachment :profile_image def full_name last_name + " " + first_name end def kana_full_name kana_last_name + " " + kana_first_name end end②FactoryBotを使用し、userデータをあらかじめ用意しておく
つまづきポイント「FactoryBotとはなんぞや」
FactoryBotについては『EverydayRails-RSpecによるRailsテスト入門』にも記載されています。
gemを入れることで使えるようになるので、FactoryBotで検索してみてもいいかもです。
私も最初分かりませんでしたが大丈夫です。
単純にテスト用のデータを記述しておき、③の@userようなインスタンス変数やローカル変数に代入できたりする便利なものってだけです。
使い方は③で記載します。まずは $ bin/rails g factory_bot:model user を実行するとファイルが作成されますので、作成されたら自身のアプリケーションのカラムに合わせてサンプルデータを入れてみてください。②spec/factories/users.rbFactoryBot.define do #FactoryBotを使用し、userデータをあらかじめ用意しておく factory :user do last_name { "テスト" } first_name { "太郎" } kana_last_name { "テスト" } kana_first_name { "タロウ" } email { "test@example.com" } postal_code { "1234567" } address { "東京都千代田区123-12-1" } phone_number { "12345678910" } password { "testtaro" } end end③具体的なテストの記述
いよいよテストを書いていきます。安心してください。後でちゃんとつまづきます。
$ bin/rails g rspec:model user を実行すると spec/modelsフォルダ内に user_spec.rbが作成されます。
モデルのテストコード(バリデーションや自身で作成したメソッドなど)はこちらのファイルに記述していきます。以下、完成例です。
③spec/models/user_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do before do @user = FactoryBot.build(:user) end describe "バリデーションのテスト" do it "姓、名、カナ姓、カナ名、メール、郵便番号、住所、電話番号、パスワードがあれば有効な状態であること" do expect(@user).to be_valid end it "姓がなければ無効な状態であること" do @user.last_name = "" @user.valid? expect(@user.errors[:last_name]).to include("を入力してください") end it "姓が10文字以下でなければ無効であること" do @user.last_name = "a" * 11 @user.valid? expect(@user.errors[:last_name]).to include("は10文字以内で入力してください") end it "名がなければ無効な状態であること" do @user.first_name = "" @user.valid? expect(@user.errors[:first_name]).to include("を入力してください") end it "名が10文字以下でなければ無効であること" do @user.first_name = "a" * 11 @user.valid? expect(@user.errors[:first_name]).to include("は10文字以内で入力してください") end it "カナ姓がなければ無効な状態であること" do @user.kana_last_name = "" @user.valid? expect(@user.errors[:kana_last_name]).to include("を入力してください") end it "カナ姓が10文字以下でなければ無効であること" do @user.kana_last_name = "a" * 11 @user.valid? expect(@user.errors[:kana_last_name]).to include("は10文字以内で入力してください") end it "カナ名がなければ無効な状態であること" do @user.kana_first_name = "" @user.valid? expect(@user.errors[:kana_first_name]).to include("を入力してください") end it "カナ名が10文字以下でなければ無効であること" do @user.kana_first_name = "a" * 11 @user.valid? expect(@user.errors[:kana_first_name]).to include("は10文字以内で入力してください") end it "メールアドレスがなければ無効な状態であること" do @user.email = "" @user.valid? expect(@user.errors[:email]).to include("を入力してください") end it "メールアドレスが30文字以下でなければ無効な状態であること" do @user.email = "a" * 31 @user.valid? expect(@user.errors[:email]).to include("は30文字以下で入力してください。") end it "郵便番号がなければ無効な状態であること" do @user.postal_code = "" @user.valid? expect(@user.errors[:postal_code]).to include("を入力してください") end it "郵便番号が7文字未満であれば無効な状態であること" do @user.postal_code = "a" * 6 @user.valid? expect(@user.errors[:postal_code]).to include("は7文字以上で入力してください") end it "郵便番号が7文字を超えると無効な状態であること" do @user.postal_code = "a" * 8 @user.valid? expect(@user.errors[:postal_code]).to include("は7文字以内で入力してください") end it "住所がなければ無効な状態であること" do @user.address = "" @user.valid? expect(@user.errors[:address]).to include("を入力してください") end it "住所が30文字以下でなければ無効な状態であること" do @user.address = "a" * 31 @user.valid? expect(@user.errors[:address]).to include("は30文字以内で入力してください") end it "電話番号がなければ無効な状態であること" do @user.phone_number = "" @user.valid? expect(@user.errors[:phone_number]).to include("を入力してください") end it "電話番号が12文字以下でなければ無効な状態であること" do @user.phone_number = "a" * 13 @user.valid? expect(@user.errors[:phone_number]).to include("は12文字以内で入力してください") end it "自己紹介文が200文字以下でなければ無効な状態であること" do @user.introduction = "a" * 201 @user.valid? expect(@user.errors[:introduction]).to include("は200文字以内で入力してください") end it "パスワードが6文字以上でなければ無効であること" do @user.password = "a" * 5 @user.valid? expect(@user.errors[:password]).to include("は6文字以上で入力してください。") end it "重複したメールアドレスなら無効な状態であること" do FactoryBot.create(:user) @user.valid? expect(@user.errors[:email]).to include("は既に存在します。") end end describe "インスタンスメソッドのテスト" do it "ユーザーのフルネームを文字列として返すこと" do @user.last_name = "テスト" @user.first_name = "太郎" expect(@user.full_name).to eq "テスト 太郎" end it "ユーザーのカナフルネームを文字列として返すこと" do @user.kana_last_name = "テスト" @user.kana_first_name = "タロウ" expect(@user.kana_full_name).to eq "テスト タロウ" end end end「ふむふむ、、テストコードはこのファイルに書くのね。了解了解 って1行目からよくわからんやん! require 'rails_helper' って何やね〜ん」って思った方。
これはRSpecに対し、ファイル内のテストを実行するためにRailsアプリケーションの読み込みが必要であることを伝えています。この記述はテストスイート内のほぼすべてのファイルで必要になります。(EverydayRailsから引用)
なんかEverydayRailsあれば別に説明要らなくね...?と思い始めてきましたが、めげずに頑張ります。要するに require 'rails_helper' という記述をすることで、RSpec先輩に対し
「僕のアプリケーションこんな感じです!モデルにはこんなデータがあって、コントローラはこんな記述してて、ビューではこんなものを表示させてます!把握お願いします!」
ということを伝えています。
この記述のおかげで、RSpecがアプリケーションと記述したコードを照らし合わせてテストを行ってくれます。じゃあ、その rails_helper って何なの?って思った方。
Rspecの設定はspecフォルダ内の rails_helper.rb に書いたりします。
次回記載予定のシステムスペックなどで使用する、deviseのヘルパーメソッドをsystem_spec内で使用可能にする設定もここに書いています。この設定の部分などについてもEverydayRailsに記載されてます。
もう本当にEverydayRails頼りになってますが、それくらいテストコードの具体的な記述や、なぜこの記述が必要か、などの情報が網羅されている書籍なのでおすすめです。spec/rails_helper.rb# deviseのヘルパーメソッドをsystem_spec内で使用可能にする config.include Devise::Test::IntegrationHelpers, type: :system
「ほうほう、rails_helperについては何となく分かった。でもその下の完成コードだけ示されてもよく分からんよう」って方もいると思うので、一応簡単に説明はしていきます。
ただ、ユーザーモデルのテストに関してはEverydayRailsにより詳しく書いているので、そちらを参考にした方がいいかもです。以下のコード例をご覧下さい。
まず、beforeブロックは同ファイル内の複数のテストで同じデータが必要な場合に、コードをDRYにする為に使用したりします。
今回の場合、右辺の FactoryBot.build(:user) という記述によって、先程の②で記述したspec/factories/users.rbのテストデータをbuildして@userに代入してあげています。
そうすることで、後のitブロックなどで@userが使えるようになります。
itブロックの1行目に @user.last_name = "" で last_name のデータを空のまま上書きして、「@user.last_nameがnilなら無効だよ〜」とか、 @user.last_name = "a" * 11 で last_name に "aaaaaaaaaaa" というデータを上書きし、「last_nameはバリデーションかけてるから10文字以下じゃないと無効だよ〜」とかのテストを書いているという感じです。
ちなみにその前にある be_valid はマッチャといいます。
マッチャに関しても伊藤さんの使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」
を参考にするといいかと思います。つまづきポイント「FactoryBotの記述位置」
また、RSpec.describe User, type: :model doの直下にbeforeブロックを置き、その中にFactoryBotで作成した@userを記述していることで、beforeブロック内の@userは、同ファイル内の全テストで使うことができるようになっています。
今回はまだ単純なアソシエーションのみなのでマシなのですが、アソシエーションが増えてくるにつれ、FactoryBotでデータを作成することが多くなり、その際に記述する位置がちゃんとしていないとハマります。
特にSystemSpecですね。沼でした。後に話していこうと思います。③spec/models/user_spec.rbRSpec.describe User, type: :model do # beforeブロックでは同ファイル内の複数のテストで同じデータが必要な場合に、コードをDRYにする為に使用する。 before do @user = FactoryBot.build(:user) # @userにはlast_name{ "テスト" }、first_name { "太郎" }とかのデータが入っている end it "姓、名、カナ姓、カナ名、メール、郵便番号、住所、電話番号、パスワードがあれば有効な状態であること" do expect(@user).to be_valid end it "姓がなければ無効な状態であること" do @user.last_name = "" # @user.last_nameがnilなら無効だよ〜 @user.valid? expect(@user.errors[:last_name]).to include("を入力してください") end it "姓が10文字以下でなければ無効であること" do @user.last_name = "a" * 11 # last_nameはバリデーションかけてるから10文字以下じゃないと無効だよ〜 @user.valid? expect(@user.errors[:last_name]).to include("は10文字以内で入力してください") end
また、以下のテストのみちょっと様子が違います。1行目にFactoryBot.create(:user)が挟まれていますね。
前述したbeforeブロックではitブロックの前に@userを作ってくれています。このテストもそうです。
ただし、今回のbeforeブロック内の@userはcreateではなく、buildなので、saveされない限りデータベースには保存されません。
以下のテストでは1行目でFactoryBot.create(:user)と記述することで、順番としてはbeforeブロック内の@userよりも先にこちらが保存されることになります。
その後に @user.valid?(@userは有効ですか?) と聞くと、先に同じデータが保存されているので、include("は既に存在します。")という文言が期待されるわけですね。③spec/models/user_spec.rbit "重複したメールアドレスなら無効な状態であること" do FactoryBot.create(:user) # 先に保存される @user.valid? expect(@user.errors[:email]).to include("は既に存在します。") end残りのitブロックも構造は同じで、恐らく何となくは理解できると思うので、端折ります。
注意したいのが、expect(エクスペクテーション)のinclude("を入力してください")の部分は、人によってそれぞれエラー文が異なるかと思うので、
i18nによるエラーの日本語化などしている方は devise.ja.yml などで確認、もしくは実行時のターミナルのエラー文にもヒントがあるのでそれぞれ確認して文言を入れてあげましょう。
最後にメソッドのテストをします。
伊藤さんもRSpecビギナーズの動画内で仰ってましたが、モデルのバリデーションのテストも大事だけど、自身で作成したメソッドなどをテストするのが大事ですとのこと。
また、アソシエーションのテストに関しては書かなくても良いレベルだそうです。
(ただし、名前の重複が起きてclass_nameなどを使い擬似的にモデルを作ったりした場合(フォロー機能など)は、やってもいいかなとのことでした。)
アソシエーションなどはRailsがよしなにしてくれている部分があるので、やるなら自身で作成したメソッドが正しく機能するかどうかを確かめるべきなのだそう。
確かに〜〜〜と思いました。動画内で仰っているので、是非見てみてください。
ということで、めちゃくちゃ簡単ですが以下にメソッドのテストも記述します。③spec/models/user_spec.rbdescribe "インスタンスメソッドのテスト" do it "ユーザーのフルネームを文字列として返すこと" do @user.last_name = "テスト" @user.first_name = "太郎" expect(@user.full_name).to eq "テスト 太郎" end it "ユーザーのカナフルネームを文字列として返すこと" do @user.kana_last_name = "テスト" @user.kana_first_name = "タロウ" expect(@user.kana_full_name).to eq "テスト タロウ" end endまあ、特に説明なしでも分かるくらい簡単なものなので敢えて説明はしません。
①のモデルに記述しているfull_nameメソッドとkana_full_nameメソッドがそれぞれちゃんとフルネームになっているか、というだけです。
メソッドのテストに関しては、複雑なものは私も書けていないので勉強します。。法人ユーザーモデルのテスト
こちらも実際にはRelationshipモデルなどのアソシエーションがありますが、今回は扱わないので敢えて削除しております。
法人ユーザーのテストは個人ユーザーとほぼ一緒なのでコードのみ記載しますので以下ご参考までに。①company.rbclass Company < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :rooms, dependent: :destroy has_many :messages, dependent: :destroy has_many :articles, dependent: :destroy has_many :notifications, dependent: :destroy validates :company_name, presence: true, length: { maximum: 30 } validates :kana_company_name, presence: true, length: { maximum: 30 } validates :email, presence: true, length: { maximum: 30 } validates :postal_code, presence: true, length: { minimum: 7, maximum: 7 } validates :address, presence: true, length: { maximum: 30 } validates :phone_number, presence: true, length: { maximum: 12 } validates :introduction, length: { maximum: 800 } attachment :profile_image attachment :background_image # approvedがtrueであればログイン可。新規登録時点ではdefaultがfalseなのでログインできない状態にする def active_for_authentication? super && self.approved? end # 上記でログインが弾かれた後のメッセージ。文言詳細は config/locales/devise.ja.yml に記載。 def inactive_message self.approved? ? super : :needs_admin_approval end def followed_by?(user) passive_relationships.find_by(following_id: user.id).present? end end②spec/factories/companies.rbFactoryBot.define do factory :company do company_name { "テスト株式会社" } kana_company_name { "テストカブシキガイシャ" } email { "testcompany@example.com" } postal_code { "1234567" } address { "東京都千代田区123-12-1" } phone_number { "12345678910" } password { "testcompany" } approved { true } is_active { true } end end③spec/models/company_spec.rbrequire 'rails_helper' RSpec.describe Company, type: :model do describe "バリデーションのテスト" do before do @company = FactoryBot.build(:company) end it "企業名、企業カナ名、メール、郵便番号、住所、電話番号、パスワードがあれば有効な状態であること" do expect(@company).to be_valid end it "企業名がなければ無効な状態であること" do @company.company_name = "" @company.valid? expect(@company.errors[:company_name]).to include("を入力してください") end it "企業カナ名がなければ無効な状態であること" do @company.kana_company_name = "" @company.valid? expect(@company.errors[:kana_company_name]).to include("を入力してください") end it "メールアドレスがなければ無効な状態であること" do @company.email = "" @company.valid? expect(@company.errors[:email]).to include("を入力してください") end it "郵便番号がなければ無効な状態であること" do @company.postal_code = "" @company.valid? expect(@company.errors[:postal_code]).to include("を入力してください") end it "住所がなければ無効な状態であること" do @company.address = "" @company.valid? expect(@company.errors[:address]).to include("を入力してください") end it "電話番号がなければ無効な状態であること" do @company.phone_number = "" @company.valid? expect(@company.errors[:phone_number]).to include("を入力してください") end it "パスワードが6文字以上でなければ無効であること" do @company.password = "a" * 5 @company.valid? expect(@company.errors[:password]).to include("は6文字以上で入力してください") end it "重複したメールアドレスなら無効な状態であること" do FactoryBot.create(:company) @company.valid? expect(@company.errors[:email]).to include("はすでに存在します") end end endジャンルモデルのテスト
ジャンルも特に難しいことはしていないので説明は省きます。
①genre.rbclass Genre < ApplicationRecord has_many :articles, dependent: :destroy validates :genre_name, presence: true, length: { maximum: 15 } end②spec/factories/genres.rbFactoryBot.define do factory :genre do genre_name { "テストジャンル" } is_active { true } end end③spec/models/genre_spec.rbrequire 'rails_helper' RSpec.describe Genre, type: :model do describe "バリデーションのテスト" do it "ジャンル名がなければ無効な状態であること" do @genre = FactoryBot.build(:genre) @genre.genre_name = "" @genre.valid? expect(@genre.errors[:genre_name]).to include("を入力してください") end end end記事モデルのテスト
①記事モデルの紹介
やっとここまできました。記事のモデルスペックは少しだけ異なる記述をしていたりするので簡単に説明していきたいと思います。
以下、記事モデルです。①article.rbclass Article < ApplicationRecord belongs_to :company belongs_to :genre validates :title, presence: true, length: { maximum: 35 } validates :body, presence: true attachment :image # 掲載ステータスが有効かつジャンルが有効になっている記事のみ探す def self.all_active where(is_active: true).joins(:genre).where(genres: {is_active: true}) end def favorited_by?(user) favorites.where(user_id: user.id).exists? end end②FactoryBotを使用し、articleデータをあらかじめ用意しておく
②spec/factories/articles.rbFactoryBot.define do factory :article do title { "テストタイトル" } body { "テスト本文" } is_active { true } company genre end end③具体的なテストの記述
記事モデルのテストを書いていきます。
$ bin/rails g rspec:model article で spec/modelsフォルダ内に article_spec.rbが作成されます。
以下、完成コード例です。③spec/models/article_spec.rbrequire 'rails_helper' RSpec.describe Article, type: :model do describe "Articleのテスト" do before do @company = FactoryBot.create(:company) @genre = FactoryBot.create(:genre) @article = FactoryBot.build(:article) end # article作成 context "全てのデータが入っている場合" do it "全て入力してあるので保存される" do @article.company_id = @company.id @article.genre_id = @genre.id expect(@article.save).to be true end end context "全てのデータが入っていない場合" do it "全て入力されていないので保存されない" do @article.company_id = @company.id @article.genre_id = @genre.id @article.title = "" @article.body = "" expect(@article.save).to be false end end end describe "バリデーションのテスト" do before do @article = FactoryBot.build(:article) end context "タイトルが存在しない場合" do it "無効な状態であること" do @article.title = "" @article.valid? expect(@article.errors[:title]).to include("を入力してください") end end context "タイトルが35文字を超える場合" do it "エラーメッセージが出ること" do @article.title = "a" * 36 @article.valid? expect(@article.errors[:title]).to include("は35文字以内で入力してください") end end it "本文がなければ無効な状態であること" do @article.body = "" @article.valid? expect(@article.errors[:body]).to include("を入力してください") end end endまあ、そこまで目新しいものはないのですが記事モデルは①でも記載している通り
belongs_to :company
belongs_to :genre
というようなアソシエーションになっており、今までとは少しだけ異なります。
以下の通りbeforeブロックでは先にcompanyとgenreをFactoryBotでcreateし、データベースに保存したうえで、@companyと@genreにそれぞれ代入しています。
記事投稿の流れは
会社が居て→記事ジャンルを選択し→記事を投稿できる
という流れになっています。
これを踏まえると、記事の投稿を行うには法人とジャンルが存在しなければならないので、beforeブロックは以下のような記述になっています。あとは簡単ですね。
1.どの企業が投稿した記事か, 2.記事のジャンルはどれか
それぞれ@article.company_id、@article.genre_id に代入してあげて、
1.全てのデータが入っている場合, 2.全てのデータが入っていない場合 にcontextで分けてテストするだけです!③spec/models/article_spec.rbRSpec.describe Article, type: :model do describe "Articleのテスト" do before do # 会社が居て→記事ジャンルを選択し→記事を投稿できる @company = FactoryBot.create(:company) @genre = FactoryBot.create(:genre) @article = FactoryBot.build(:article) end # article作成 context "全てのデータが入っている場合" do it "全て入力してあるので保存される" do @article.company_id = @company.id @article.genre_id = @genre.id expect(@article.save).to be true end end context "全てのデータが入っていない場合" do it "全て入力されていないので保存されない" do @article.company_id = @company.id @article.genre_id = @genre.id @article.title = "" @article.body = "" expect(@article.save).to be false end end endこれでモデルのテストは終わりです!
メッセージモデルのモデルスペックもあるのですが、項目数が少ないため省略致します。
気になる方はGitHubからご覧ください。終わりに
今回はアプリケーションのコア部分であるモデルのテスト、モデルスペックについてまとめました。
モデルスペックも大事なのですが、特に大事なのは次回記載するシステムスペックになります。
ユーザーがマイページを編集したり、法人にDMを送ったりなどの、実際のブラウザでの動きをテストします。
つまづくポイントも多かったです。私だけかもしれませんが
なるべく分かりやすくまとめていければと思いますので、よろしければ次回も見ていただけると嬉しいです。
最後までご覧いただきありがとうございました。2020.09.28 システムスペック編を掲載しました。こちらをご覧下さい。
参考記事
https://qiita.com/jnchito/items/2a5d3e15761fd413657a
https://qiita.com/jnchito/items/42193d066bd61c740612
https://qiita.com/jnchito/items/2e79a1abe7cd8214caa5
- 投稿日:2020-09-27T17:59:57+09:00
CodeWarでの勉強(ruby)③ squeeze, gsub
この記事について
最近始めたCodewarを通じて学べたことを少しずつアウトプット
問題
Implement the function unique_in_order which takes as argument a sequence and returns a list of items without any elements with the same value next to each other and preserving the original order of elements.
関数unique_in_orderを使って引数としてシーケンス?を受け取り、同じ値が隣接してなくて、要素の順番にしてリストを返すようにさせる。
unique_in_order('AAAABBBCCDAABBB') == ['A', 'B', 'C', 'D', 'A', 'B'] unique_in_order('ABBCcAD') == ['A', 'B', 'C', 'c', 'A', 'D'] unique_in_order([1,2,2,3,3]) == [1,2,3]僕が考えた方法
①繰り返して使われている値を消そうと思って、受け取った引数を配列に直してから
uniq
にしようと思ったけど、以下のようになるから失敗。def unique_in_order(iterable) iterable.chars.uniq end unique_in_order('AAAABBBCCDAABBB') =>["A", "B", "C", "D"]②連続した値を一纏めにできないかと思って検索すると、
squeezeメソッド
を知った。
https://docs.ruby-lang.org/ja/latest/method/String/i/squeeze.htmlsqueeze(*chars) -> String[permalink][rdoc][edit]
chars に含まれる文字が複数並んでいたら 1 文字にまとめます。def unique_in_order(iterable) iterable.squeeze.chars end unique_in_order('AAAABBBCCDAABBB') =>["A", "B", "C", "D", "A", "B"]できたぜ!!!!
しかしまだ終わらない
提出すると以下のように3つのことで怒られた❤️
① <NoMethodError: undefined method `squeeze' for []:Array> ② <NoMethodError: undefined method `squeeze' for [1, 2, 3, 3]:Array> ③ <NoMethodError: undefined method `squeeze' for ["a", "b", "b"]:Array>全てに共通しているのは配列に対して
suqueeze
は使えないということだ。つまり
引数が文字列の場合に
iterable.squeeze.chars
を実行して、配列の場合はiterable.uniq
をやってやったらいいんじゃないのか?成功したぜ!!!
def unique_in_order(iterable) if iterable.class == String iterable.squeeze.chars else iterable.uniq end end理想の回答
def unique_in_order(iterable) case iterable when String iterable.gsub(/(.)\1*/, '\1').split('') when Array iterable.uniq end endあ、ちょ待って。分からへん(笑)
gsubメソッド
gsub(pattern, replace) -> String
文字列中で pattern にマッチする部分全てを文字列 replace で置き換えた文字列を生成して返します。#正規表現を使わない場合 #文字列.gsub(置換したい文字列, 置換後の文字列) #正規表現を使う場合 #文字列.gsub(/正規表現/, 正規表現に該当した箇所を置換した後の文字列) p 'abcdefg'.gsub(/def/, '!!') # => "abc!!g" p 'abcabc'.gsub(/b/, '<<\&>>') # => "a<<b>>ca<<b>>c" p 'xxbbxbb'.gsub(/x+(b+)/, 'X<<\1>>') # => "X<<bb>>X<<bb>>" p '2.5'.gsub('.', ',') # => "2,5"正規表現を使ってやっているのか〜。
ここに関してはもうちょっと調べてから更新するようにします!
- 投稿日:2020-09-27T17:03:25+09:00
f.collection_selectの"選択して下さい"を選んだときのバリデーション
1.やりたいこと
<%= f.collection_select :address_id, @customer.addresses, :id, :full_address, :include_blank => "選択してください" %>
フォームにおいて選択せずに一番上の"選択して下さい"を選んだ際にバリデーションをかけたい。2.実装する
app/controllers/orders_controller.rbparams[:order][:address_id] == "" flash[:notice] = "選択して下さい" redirect_to new_order_pathパラメータから送られている値を見ると
""
となっていたのでイコール文を""
に変更。3.終わりに
これを実装したかった時にQiitaやGoogleで検索しても全然出てこなかったので
投稿しました。
しっかりターミナルを見て返ってきてる値を確認しないといけないと痛感しました。
- 投稿日:2020-09-27T16:27:10+09:00
RailsアプリケーションにTailwindを入れてみる
https://github.com/fukadashigeru/tailwind_sample_app/pull/1
環境
ruby '2.7.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 6.0.3', '>= 6.0.3.3' gem 'webpacker', '~> 4.0'準備
Slim
Gemfileに下記追加
gem 'slim-rails' gem 'html2slim'ターミナルで
bundle install先にビュー用のerbファイルを作成していたら、下記コマンドをターミナルで打つ
bundle exec erb2slim app/views app/views -dScaffold
ターミナルで下記打つ
bin/rails g scaffold blog content:textconfig/routes.rbに下記追加
root 'blogs#index'DB作成
bin/rails db:create bin/rails db:migrateTailwindを入れてみる
ref: tailwindcss Documentation
Tailwind用のスタイルを当てる
1.Install Tailwind via npm
# Using npm npm install tailwindcss # Using Yarn yarn add tailwindcss2.Add Tailwind to your CSS
app/javascript/src/scss/application.scssを追加
@import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities";app/javascript/packs/application.jsに下記追加
import '../src/scss/application.scss'3.Create your Tailwind config file (optional)
npx tailwindcss init4.Process your CSS with Tailwind
postcss.config.jsに下記追加
module.exports = { plugins: [ // ... require('tailwindcss'), require('autoprefixer'), // ... ] }確認
ターミナルでアプリケーションを立ち上げる
bin/rails s別のターミナルでwebpackerを立ち上げる(これは不要?)
bin/webpack-dev-serverローカルホストにアクセス
http://localhost:3000/Taiwind
- 投稿日:2020-09-27T15:03:19+09:00
バイナリーサーチを使って配列の中に任意の値があるか確認する
既にいろんな方が書いている話ではありますが、
書くことによって自分の中で理解を深められたら、と思ったので書きます。問題
下記の問題をバイナリーサーチを使って解いてみます。
配列
array=[1, 3, 5, 6, 9, 10, 13, 20, 26, 34]
があり、
この配列に任意の値が存在するかどうかを検索するコードを作成する。
任意の値が配列内に存在しない場合は、「値は配列内に存在しません」と表示し、
存在する場合は、配列の何番目にあるかを表示する。# 出力例1 検索したい数字を入力してください 5 5は配列の2番目に存在します # 出力例2 検索したい数字を入力してください 8 8は配列内に存在しませんそもそもバイナリーサーチとは何か
ソート済みのリストや、配列に入ったデータ(同一の値はないものとする)に対する検索を行うとき、
中央の値を見て、検索したい値との大小関係を用いて、
検索したい値が中央の値の右にあるか、左にあるかを判断して、
片側には存在しないことを確かめながら検索していく方法のこと。
1回の処理で選択肢が半分になるので、処理速度の向上が期待できる。
二分探索、二分割検索ともいう。どんな感じなのかざっくり書いてみる
検索したい値は
target
、
一番左側の添え字はleft
、
一番右側の添え字はright
、
真ん中の添え字はcenter
に代入することとします。ちなみに今回はtarget = 5として検索します。
配列を図にすると下記のようになります。
それでは実際にやってみます。1回目の検索では、
left = 0
right = 9
となり、中央の値がある添え字は
center = (left + right) / 2、つまりcenter = 4となる。
(本当は9を2で割ると4.5だが、Rubyの場合、整数 / 整数 = 整数(小数点以下切り捨て)になるのでこれで大丈夫)
中央の値がある添え字が分かったので検索したい値と比較する。
array[center] = 9
target = 5
なので、array[center] > targetとなる。
これで次に検索すべき範囲がわかる。下の図のようになる。グレーが検索対象外。
図を見て分かる通りrightが変更になる。
centerの一つ左側へ変更になるので、
right = center - 1もちろんcenterも変わる。求め方は1回目と同じ。
center = (left + right) / 2
ちなみに2回目は計算するとcenter = 1になる。そして1回目と同じように中央の値と検索したい値を比較。
array[center] = 3
target = 5
なので、array[center] < targetとなる。そして次の検索範囲が分かる。
今度はleftが変わる。centerの1つ右になるので
left = center + 1
centerも変わる。求め方はこれまでと同じ。
center = (left + right) / 2
ちなみに3回目は計算するとcenter = 2になる。そしてこれまでと同じように中央の値と検索したい値を比較。
array[center] = 5
target = 5
array[center] == target となり検索終了。回答例
上記のざっくり書いたものを整理しながら書き直してみます。
def binary_search(array, right, target) left = 0 while left <= right center = (left + right) / 2 if array[center] == target return center elsif array[center] > target right = center - 1 else left = center + 1 end end return -1 end array = [1, 3, 5, 6, 9, 10, 13, 20, 26, 34] # 16行目 puts "検索したい数字を入力してください" # 18行目 target = gets.to_i elements_num = array.count - 1 result = binary_search(array, elements_num, target) # 22行目 if result == -1 # 24行目 puts "#{target}は配列内に存在しません" else puts "#{target}は配列の#{result}番目に存在します " end補足
binary_searchメソッドの中にバイナリーサーチに関する記述をしています。
whileで繰り返し処理するように設定しておきます。
繰り返しが有効になる条件は、配列の一番左側の添え字(left)が配列の一番右側の添え字(right)と同じになるまでとしたい(超えたらwhile内の処理はしない)ので、left <= rightとします。array[center] == targetとなった場合は、
centerの値を返すことと、returnでメソッド内の処理から抜け出すようにしています。20行目のelements_num = array.count - 1で、配列の一番右側の添え字を取得しています。
countじゃなくてlengthでも良いのかも。もしソートされてない配列だったら
配列名.sort
で並び替えができます。おわり
eachメソッド使って比べていく方法もありますが、配列の中身が多いほどバイナリーサーチの方が便利ですね。
そういえば昔に友達との間で流行った某アプリってこの仕組みを使用してたのかな。なんだか長くなってしまいました。最後までお付き合いありがとうございました。
- 投稿日:2020-09-27T14:28:48+09:00
Basic認証を交えたテストコードの書き方
【概要】
1.結論
2.○○になるのはどういう時か
補足:開発環境
1.結論
環境変数を変数に埋め込み、visitでその環境変数を埋め込んだURLに飛ぶようにする!
2.どのように記載するのか
def basic_pass(path) #---❶ username = ENV["STUDY"] password = ENV["STUDY_password"] visit "http://#{username}:#{password}@#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}#{path}" end RSpec.describe 'コメント投稿', type: :system do before do @time = FactoryBot.create(:time) @comment = Faker::Lorem.sentence end it 'ログインしたユーザーは自己学習投稿の詳細ページでコメントできる' do # ログインする basic_pass new_user_session_path #---❷ fill_in 'Email', with: @time_report.user.email fill_in 'Password', with: @time_report.user.password find('input[name="commit"]').click expect(current_path).to eq root_path end end上記のように記載しました!
❶下記のURLの具体例は環境変数になっていないので、変数に環境変数を代入する形にしました。あとは下記のURLを真似させていただきました。
❷basic_passメソッドを結合テストコードが読み込まれる前に記載しないとBasic認証テストのID,パスワードを通過できません。なので、結合テストコードの内容の一番最初にコーディングし、新規登録画面(devise gem使用)に遷移するようにしています。かなり参考にしたURL:
Capybara + Headless Chrome (System Spec) で Basic認証 を通過する方法補足:開発環境
Ruby 2.6.5
Rails 6.0.3.3
MySQL
Visual Studio Code
(Caprybara,Rspec,GoogleChrome)
- 投稿日:2020-09-27T12:27:04+09:00
[rails] NOFILEのマイグレーションファイルを削除
事件内容
このようなファイルができてしまい、
rollbackができないため、マイグレーションファイルをdownさせることができないup 20200926110535 ********** NO FILE **********事件が起こった経緯
おそらくマイグレーションのステータスがUPの状態で
マイグレーションファイルを削除してしまった。私の場合はgitデスクトップでchangeを丸ごと削除したので、この事件が起きた模様。
解決策
えっと、皆様が載せてくださっていた記事を何個か見させていただいた結果、無事解決いたしました。
まず、no faileに名前を付与するために
ターミナルでこのコマンドを実行してください
(マイグレーションIDはあなた様のnofileになっているIDに変えてください。
その後の名前はダミーですのでなんでも構いません。)% touch db/migrate/20200926110535_fuwafuwa.rb実行後↓
20200926110535_fuwafuwa.rbclass Hoge < ActiveRecord::Migration[5.2] def change end endそうすれば、上記の名前マイグレーションファイルが
存在しているはずです。バージョンはあなた様のrailsのバージョンを指定してください!!!!!
その後, rails db:migrate:statusで確認するとこのようになっていれば成功です。
up 20200926110535 fuwafuwaあとは、先ほどのマイグレーションファイルを削除すれば無事解決!
- 投稿日:2020-09-27T11:33:31+09:00
しがないRailsエンジニア2年生が2年間の振り返り
初めまして。
あと1ヶ月ほどでエンジニアになって2年経ちます。
振り返りついでにその時感じた課題の変遷をまとめようと思いました。
(一旦Railsに関することだけ抜き出しています)なんとか生き残ってるエンジニアの課題を共有できれば幸いです。
エンジニア歴
- 1ヶ月目
- Railsは半年ほど独学でやってきたものの全体的にあまり理解できていなかった
- Rails以外にもエンジニアの仕事の仕方やITの概念が必要で覚えることが多かった
- 3ヶ月目
- 何となくRailsが使えてるだけで天狗になっていた時期
- 動けば良いや精神
- 6ヶ月目
- 動くものは作れるようになったけどこの時は適当に書いて動くからヨシとしていた
- 1年目
- Railsのコードを俯瞰して追えるようになり全くわからなくなった
- 過去の自分が恥ずかしくなった
- Rails以外にReactやサーバ知識など、覚えることが多くRailsは後回しにしていた
- 1年2ヶ月目
- Railsそっちのけでインフラ周りに注力していた
- Railsはまだ動けば良いや精神
- 1年7ヶ月目
- API初挑戦
- Rails全然理解していないと自覚して焦りだす
- N+1を気をつけるようになる
- やっとクラスの使い方やRailsの動きや設定周りを理解(した気がする)
- やっとmapやAR(ActiveRecord)の動きを追えるようになる
- さらに焦る
- 1年10ヶ月目
- ModuleやClassが少し分かるようになり調子に乗る
- include, expendの違いが分かり調子に乗る
- OOD(オブジェクト指向設計)ができなくプログラミング自体分からない時期突入
- Rails wayが分からなくて泣く
- 共通化ができなくて泣く
- 3年目(これから)
- Rails wayを身に付けたい
- 処理を見て非効率な部分を改善できるようになりたい
- セキュアな書き方も身に付けたい
- バックエンドに絞って頑張っていきたい
ざっと書いてみました。
最近感じたことしか覚えてないのでメモ書きです。思い出した時に追記していきます。
自分用のメモ書きにはなりますが誰かのモチベや目標に繋がれば嬉しいです。
- 投稿日:2020-09-27T11:05:36+09:00
[Ruby on Rails]データ登録時に重複したレコードがあった場合、登録させない
背景
オリジナルアプリをデプロイ後、重複したデータを登録されていることに気づきます。
アプリの仕様としては、登録するレコードは重複させたくないのです。
データベースを確認すると、
登録するユーザが異なっていれば、登録できてしまうことが判明。
解決にそこそこ時間がかかったので、備忘として記録します。
問題箇所
controllerにデータ登録時の条件を設けていました。
require 'rubygems' require 'mechanize' def create f = (params[:wiki_url]) unless f.start_with?("https://ja.wikipedia.org/wiki/") flash[:notice] = "無効なURLが入力されたため保存できませんでした。" redirect_to action: 'new' return else agent = Mechanize.new page = agent.get(f) page.encoding='utf-8' 以下、省略...unless文には、入力したデータ(URL)に「https://ja.wikipedia.org/wiki/」
が含まれていない場合は、登録させないという条件を組んでいました。しかし、重複したレコードがある場合、登録させないという条件が含まれていなかったため、
写真のような同じデータが登録されてしまったわけです。解決(結論)
以下のように書き換えて、解決しました。
require 'rubygems' require 'mechanize' def create f = (params[:wiki_url]) if not f.start_with?("https://ja.wikipedia.org/wiki/") flash[:notice] = "無効なURLが入力されたため登録できませんでした。" redirect_to action: 'new' return elsif Company.where(page_url: "#{f}").count >= 1 flash[:notice] = "登録済みのURLが入力されたため登録できませんでした。" redirect_to actiont: 'new' return else agent = Mechanize.new page = agent.get(f) page.encoding = 'utf-8' 以下、省略...teratailの記事が参考になり、解決に至りました。
同じ名前のデータが2件以上登録されているレコードをActiveRecordを用いて取得したいです。変更点は大きく2つです。
- unless文からif not文に変更
- whereメソッド追加(ココがポイント!)
unless文からif not文に変更
ruby unless文にelsifはないよ。。。
だそうです。不正データの入力防止の条件に加えて、重複防止の条件を追加したい。
しかし、unless文にelsifが使えないので、
if not文に変えてelsifを追加しました。whereメソッド追加(ココがポイント!)
条件(処理)の流れは以下の通りです。
- whereメソッドで、テーブル内の条件に一致したレコードの数を取得する
- 取得したデータの数を条件にかけて1以上(データがあるか)判定する
- 1以上(データがあり)ならば、受付けない
whereメソッドで、テーブル内の条件に一致したレコードの数を取得する
.where("条件")で条件にあうレコードを取得できます。
モデル名.where("条件").
今回、取得するのは Companyモデル の page_urlカラムです。Company.where(page_url).
さらに、page_urlカラム内に入力したデータと一致するものを検索にかけたいので、
下記のように付け加えます。Company.where(page_url: "#{f}").
#{f}は変数内の文字列を表しています。
変数fは以下の場所で定義しており、中身はこのようになっています。require 'rubygems' require 'mechanize' def create f = (params[:wiki_url]) <= ココです。 以下、省略....
page_urlカラム内で検索をかけた結果、いくつあったか判定するために
countメソッドを追加します。Company.where(page_url: "#{f}").count取得したデータの数を条件にかけて1以上(データがあるか)判定する
1以上、つまり入力したデータと同じURLの数を数えて1以上あるか確認します。
Company.where(page_url: "#{f}").count >= 11以上(データがあり)ならば、受付けない
おまけ
アプリに反映させているので、是非遊んでみてください。
Unsung:hero
- 投稿日:2020-09-27T10:21:43+09:00
関連付しているモデルのレコードが消せずハマった
作業環境
Rails '5.2.3'
Ruby '2.7.1'
PostgreSQL何にハマったか
レビュー共有アプリ作成中にローカル環境で動作確認中、登録されているitemを削除しようとしましが
ActiveRecord::InvalidForeignKey in ItemsController#destroy
PG::ForeignKeyViolation: ERROR: update or delete on table "reviews" violates foreign key constraint "fk_rails_5350d1b47c" on table "comments" DETAIL: Key (id)=(6) is still referenced from table "comments". : DELETE FROM "reviews" WHERE "reviews"."id" = $1
というエラー、、。
on table "comments"
ってコメントテーブルなんてないぞ。と頭を悩ませていましたが、アプリ作成当初にreviewに対してコメント機能を作ろうと考えていたがreviewに対して一方的なコメント機能なんて必要ないのでは?と考え実装をやめたという過去があったのです(忘れていた)。
ですが、commentsテーブルは消したものと思いこんでおりハマってしまったのですねハマった背景
レビュー共有アプリをRailsで作成中、itemが複数のreviewを持っているという関係です。
item.rbhas_many :category_items, dependent: :destroy has_many :categories, through: :category_items has_many :reviews, dependent: :destroy has_many :favorites, dependent: :destroy accepts_nested_attributes_for :category_itemsreview.rbbelongs_to :user belongs_to :item has_many :notifications, dependent: :destroyエラーの原因は何か考える
ここからは初学者の考えた考察です。違っていたらご指摘いただけると幸いです。
itemを削除しようとすると同時にitemに関連したreviewも削除されます。
has_many :reviews, dependent: :destroy
これ。
で今回は、item削除→review消える→commentテーブルが残っている上にreview.rb(省略) has_many :comments, dependent: :destroyという記述がないからreview消せない、よってitemも消せないという現象かと考えました。
ではこのエラーにどう向き合うか
まずはcommentsテーブル本当に残っているのかという確認からですよね。
rails db:schema:dump
でdb/schema.rbを更新。
その後schema.rbを見てみるとschema.rbcreate_table "comments", force: :cascade do |t| (省略) endcommentsいた、、。不要なテーブルは消しましょう。
commentsテーブル削除
migrattionファイルを作成し、以下を記述して
def change drop_table :comments endrails db:migrateでさようなら。これでitem共にreviewの削除ができるようになりました!!
まとめ
たったこれだけで長々と書きましたが今回の件で得た教訓としては、機械は裏切らない、疑うのはまず自分であるということですね。思い込みはよくないです。
初投稿なので拙いところが多いですが、誰かの助けになればと思います。
- 投稿日:2020-09-27T10:03:52+09:00
【地図表示】Google Maps JavaScript APIとGeocoding APIを用いてユーザーが登録した住所から地図を表示する!
概要
Google Maps JavaScript APIとGeocoding APIを用いてユーザーが登録した住所から投稿詳細ページに地図を表示した時のことを備忘録として記録します。
環境
・ruby '2.5.7'
・rails '5.2.3'前提
・Google MapsのAPIキーを取得済であること
・投稿モデル(ここではDatespotモデル)に住所(adress)カラムがあること過程
1.投稿詳細ページの作成
投稿詳細ページは、各自の仕様に合わせて作成してください。
views/show.html.erb<div class="container"> <div class="row"> (省略) <div class="col-md-8"> <h2 class="datespot-name"><%= @datespot.name %></h2> <div class="datespot-info"> (省略) <h4 id="address">【住所】<%= @datespot.address %></h4> (省略) </div> </div> </div> </div> <%= render "map-show" %>2.地図を表示するビューを作成
地図を表示するビューを作成します。
views/_map-show.html.erb<div class="map-container"> <div class="map_wrapper"> <div id="map" class="map"></div> </div> </div> <script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_API_KEY']%>&callback=initMap"></script>取得したAPIキーは、環境変数に入れておきましょう。
stylesheets/custom.scss#map{ height: 310px; width: 550px; }地図の大きさを明示的に指定しないと表示されないので、必ず指定しましょう。
3.コールバック関数を定義する
2.で記載したコールバック関数を定義します。
javascripts/map-show.jsfunction initMap() { //地図を表示する領域の div 要素のオブジェクトを変数に代入 var target = document.getElementById('map'); //マーカーのタイトル var title = $('.datespot-name').text(); //HTMLに記載されている住所の取得 var address = document.getElementById('address').textContent; //ジオコーディングのインスタンスの生成 var geocoder = new google.maps.Geocoder(); //geocoder.geocode() にアドレスを渡して、コールバック関数を記述して処理 geocoder.geocode({ address: address }, function(results, status){ //ステータスが OK で results[0] が存在すれば、地図を生成 if (status === 'OK' && results[0]){ //マップのインスタンスを変数に代入 var map = new google.maps.Map(target, { //results[0].geometry.location に緯度・経度のオブジェクトが入っている center: results[0].geometry.location, zoom: 15 }); //マーカーの生成 var marker = new google.maps.Marker({ position: results[0].geometry.location, map: map, animation: google.maps.Animation.DROP }); //取得した座標の生成 var latlng = new google.maps.LatLng(results[0].geometry.location.lat(), results[0].geometry.location.lng()); //情報ウィンドウに表示するコンテンツを作成 var content = '<div id="map_content"><p>' + title + '<br/>' + address + '<br/><a href="https://maps.google.co.jp/maps?q=' + latlng + '&iwloc=J" target="_blank" rel="noopener noreferrer">Googleマップで見る</a></p></div>'; //情報ウィンドウのインスタンスを生成 var infowindow = new google.maps.InfoWindow({ content: content, }); //marker をクリックすると情報ウィンドウを表示(リスナーの登録) google.maps.event.addListener(marker, 'click', function() { //第2引数にマーカーを指定して紐付け infowindow.open(map, marker); }); }else{ //ステータスが OK 以外の場合や results[0] が存在しなければ、アラートを表示して処理を中断 alert("住所から位置の取得ができませんでした。: " + status); return; } }); }
var target = document.getElementById('map');
は、
views/map-show.html.erbの<div id="map" class="map"></div>
を参照しています。
var title = $('.datespot-name').text();
は、
views/show.html.erbの<h2 class="datespot-name"><%= @datespot.name %></h2>
を参照しています。
var address = document.getElementById('address').textContent;
は、
views/show.html.erbの<h4 id="address">【住所】<%= @datespot.address %></h4>
を参照しています。結果
これで、ユーザーが登録した住所から投稿詳細ページに地図を表示できました!
参考