20200927のRubyに関する記事は12件です。

Rails: rake taskを良い感じに書く方法

良い感じ = 以下の二点

  1. テストしやすいこと
    • 工数削減のため
  2. (ある程度は)エンジニア間での書き方が統一できること
    • 例えばdryrunの指定方法が書き手によってまちまちだと、商用でのtask実行時に事故が起きる可能性もあるので統一可能であればした方が良い
    • ログのフォーマットが統一されていないと作業効率が悪い

などなど

コード

lib/tasks/issue_6885.rb
require_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
end
lib/tasks/helpers/all_user_name_update_helper.rb
require_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
end

lib/tasks/helpers/rake_helper_template.rb
class 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 

ポイント

  1. .rakeファイルはhelperクラスを呼び出すだけにして、rakeのDSL?的なお作法を気にせずに普段書き慣れているclassのメソッドをテストするようにした
  2. yieldを使ってdryrun, logger(の一部)を共通化し、rake taskを作成するたびに毎回定義しなくても済むようにした
    • mainメソッド(名前は何でも良い)には極力そのtaskの処理フローだけを定義するようにし、個別具体的な処理はテストしやすい粒度で別メソッドに切り出すようにする

テンプレートにしてしまえばall_user_name_update_helper.rbのようにやりたいことに集中できる!(^○^)

所感

  • rake taskの書き方は久々に書くと忘れてるのでこの記事は実は備忘録的な意味合いもあったり
  • yieldは正直わかりにくいので使いたくない派だけど、テンプレートのように毎回書いたり読んだりしないものであれば良いかなという考え
  • 他にも良い感じの書き方あるよという方いたら教えてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【第1回】RSpecビギナーが、ビギナーなりにModelSpecを書いてみた

はじめに

はじめまして。最初に自己紹介を簡単にさせて頂きます。
2020年5月〜2020年9月までDMMWEBCAMPにてRubyを中心に学習し、現在転職活動中の卒業生です。
先日、RSpecの雄である伊藤淳一 @jnchito さんのご厚意で開催された初学者向けの勉強会(RSpecビギナーズ!!)にも参加致しました。

ポートフォリオにRSpecを用いたテストを記述する中で感じた、
私と同じ初学者なら「ここでつまづくだろうな:thinking:」とか「ここが分からん:hugging:」といった"つまづきポイント"を稚拙ながら初学者目線でまとめました。
また、やっていく中で自分が書きたい機能の具体的なテストコード例が欲しいと思うことがあったので、自分の復習も兼ねてますが、この記事がRSpecビギナーズにとって少しでも参考になれば嬉しいなと思います。
※厚かましくはありますが、この記事をたまたま見た"RSpecエキスパート"がいらっしゃいましたら、下手くそなコードに是非アドバイスいただけると幸いです。

この記事で扱うこと

  • ModelSpec(モデルスペック)
    アプリケーションのコア部分であるモデルのテスト
  • モデルスペックの具体的な記述例
    自身のポートフォリオを参考にして記述していきます

この記事で扱わないこと

  • SystemSpec(システムスペック)
    モデル、コントローラ、ビュー、全部テストできるよ! 詳細はこちらをご覧下さい。
  • RSpecのセットアップ、準備
    (後述する参考書籍『EverydayRails-RSpecによるRailsテスト入門』にて詳しく記載してあるのでそちらを参考にしてください)

前提

  • 対象
    RSpec書こうとしてるけど何が何だかさっぱりなんじゃあ〜:hugging:という初学者の方。 ただ、伊藤さんの使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 こちらの記事内容をある程度は見ていたり、なんかやったことはあるな〜とか、最低限describe,it,expectの役割が分かる方が望ましいです。
  • 参考コード
    前述したように自身のポートフォリオを参考に記述するので、こちらにGitHubのリンクを貼っておきますが、テストの対象はサイトの基幹機能に絞っていますのでご了承ください。
    【サイトの基幹機能】(個人・法人会員の新規登録/ログイン/編集、法人会員登録の申請、記事の投稿/編集、DM、通知など)
    DM・通知などは次回SystemSpec編でご紹介します。

  • テストを記述するための準備
    RSpecによるテストを記述するためには、gem 'rspec-rails'をはじめ、いくつかgemを入れたり設定をする必要があります。
    まずはテストを書く準備を整えてからお読みください。
    必要なものは『EverydayRails-RSpecによるRailsテスト入門』に記載してあります。
    というか、これを見ればこの記事を見なくても分かる人は分かると思います。
    具体例としてコードを見たいという方はそのままお読みいただけると嬉しいです。(本当に参考程度ですが)

個人ユーザーモデルのテスト

①個人ユーザーモデルの紹介

まず初めに個人ユーザーモデルをお見せします。
実際はfavoriteモデルなどのアソシエーションもありますが、今回は扱わないので敢えて削除しております。

①user.rb
class 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データをあらかじめ用意しておく

つまづきポイント:hugging:「FactoryBotとはなんぞや」
 FactoryBotについては『EverydayRails-RSpecによるRailsテスト入門』にも記載されています。
 gemを入れることで使えるようになるので、FactoryBotで検索してみてもいいかもです。
 私も最初分かりませんでしたが大丈夫です。
 単純にテスト用のデータを記述しておき、③の@userようなインスタンス変数やローカル変数に代入できたりする便利なものってだけです。
 使い方は③で記載します。まずは $ bin/rails g factory_bot:model user を実行するとファイルが作成されますので、作成されたら自身のアプリケーションのカラムに合わせてサンプルデータを入れてみてください。

②spec/factories/users.rb
FactoryBot.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.rb
require '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

「ふむふむ、、テストコードはこのファイルに書くのね。了解了解:vulcan: って1行目からよくわからんやん! require 'rails_helper' って何やね〜ん:hugging:」って思った方。
これは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については何となく分かった。でもその下の完成コードだけ示されてもよく分からんよう:hugging:」って方もいると思うので、一応簡単に説明はしていきます。
ただ、ユーザーモデルのテストに関しては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「使用頻度の高いマッチャを使いこなす」
を参考にするといいかと思います。

つまづきポイント:hugging:「FactoryBotの記述位置」
また、RSpec.describe User, type: :model doの直下にbeforeブロックを置き、その中にFactoryBotで作成した@userを記述していることで、beforeブロック内の@userは、同ファイル内の全テストで使うことができるようになっています。
今回はまだ単純なアソシエーションのみなのでマシなのですが、アソシエーションが増えてくるにつれ、FactoryBotでデータを作成することが多くなり、その際に記述する位置がちゃんとしていないとハマります。
特にSystemSpecですね。沼でした。後に話していこうと思います。

③spec/models/user_spec.rb
RSpec.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.rb
it "重複したメールアドレスなら無効な状態であること" 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.rb
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

まあ、特に説明なしでも分かるくらい簡単なものなので敢えて説明はしません。
①のモデルに記述しているfull_nameメソッドとkana_full_nameメソッドがそれぞれちゃんとフルネームになっているか、というだけです。
メソッドのテストに関しては、複雑なものは私も書けていないので勉強します。。

法人ユーザーモデルのテスト

こちらも実際にはRelationshipモデルなどのアソシエーションがありますが、今回は扱わないので敢えて削除しております。
法人ユーザーのテストは個人ユーザーとほぼ一緒なのでコードのみ記載しますので以下ご参考までに。

①company.rb
class 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.rb
FactoryBot.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.rb
require '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.rb
class Genre < ApplicationRecord
  has_many :articles, dependent: :destroy
  validates :genre_name, presence: true, length: { maximum: 15 }
end
②spec/factories/genres.rb
FactoryBot.define do
  factory :genre do
    genre_name { "テストジャンル" }
    is_active { true }
  end
end
③spec/models/genre_spec.rb
require '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.rb
class 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.rb
FactoryBot.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.rb
require '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.rb
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

これでモデルのテストは終わりです!
メッセージモデルのモデルスペックもあるのですが、項目数が少ないため省略致します。
気になる方はGitHubからご覧ください。

終わりに

今回はアプリケーションのコア部分であるモデルのテスト、モデルスペックについてまとめました。
モデルスペックも大事なのですが、特に大事なのは次回記載するシステムスペックになります。
ユーザーがマイページを編集したり、法人にDMを送ったりなどの、実際のブラウザでの動きをテストします。
つまづくポイントも多かったです。私だけかもしれませんが:thinking:
なるべく分かりやすくまとめていければと思いますので、よろしければ次回も見ていただけると嬉しいです。
最後までご覧いただきありがとうございました。

2020.09.28 システムスペック編を掲載しました。こちらをご覧下さい。

参考記事

https://qiita.com/jnchito/items/2a5d3e15761fd413657a
https://qiita.com/jnchito/items/42193d066bd61c740612
https://qiita.com/jnchito/items/2e79a1abe7cd8214caa5

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

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.html

squeeze(*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"

正規表現を使ってやっているのか〜。
ここに関してはもうちょっと調べてから更新するようにします!

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

f.collection_selectの"選択して下さい"を選んだときのバリデーション

1.やりたいこと

<%= f.collection_select :address_id, @customer.addresses, :id, :full_address, :include_blank => "選択してください" %>
フォームにおいて選択せずに一番上の"選択して下さい"を選んだ際にバリデーションをかけたい。

2.実装する

app/controllers/orders_controller.rb
 params[:order][:address_id] ==  ""
 flash[:notice] = "選択して下さい"
 redirect_to new_order_path

パラメータから送られている値を見ると""となっていたのでイコール文を""に変更。

3.終わりに

これを実装したかった時にQiitaやGoogleで検索しても全然出てこなかったので
投稿しました。
しっかりターミナルを見て返ってきてる値を確認しないといけないと痛感しました。

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

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 -d

Scaffold

ターミナルで下記打つ

bin/rails g scaffold blog content:text

config/routes.rbに下記追加

root 'blogs#index'

DB作成

bin/rails db:create
bin/rails db:migrate

Tailwindを入れてみる

ref: tailwindcss Documentation

Tailwind用のスタイルを当てる

image

1.Install Tailwind via npm

# Using npm
npm install tailwindcss

# Using Yarn
yarn add tailwindcss

2.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 init

4.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/

tailwindのスタイルがあたるようになっている
image

Taiwind

https://tailwindcss.com/

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

バイナリーサーチを使って配列の中に任意の値があるか確認する

既にいろんな方が書いている話ではありますが、
書くことによって自分の中で理解を深められたら、と思ったので書きます。

問題

下記の問題をバイナリーサーチを使って解いてみます。

配列 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として検索します。

配列を図にすると下記のようになります。
SC1.png
それでは実際にやってみます。

1回目の検索では、
left = 0
right = 9
となり、中央の値がある添え字は
center = (left + right) / 2、つまりcenter = 4となる。
(本当は9を2で割ると4.5だが、Rubyの場合、整数 / 整数 = 整数(小数点以下切り捨て)になるのでこれで大丈夫)
中央の値がある添え字が分かったので検索したい値と比較する。
array[center] = 9
target = 5
なので、array[center] > targetとなる。
これで次に検索すべき範囲がわかる。下の図のようになる。グレーが検索対象外。
SC2.png
図を見て分かる通りrightが変更になる。
centerの一つ左側へ変更になるので、
right = center - 1

もちろんcenterも変わる。求め方は1回目と同じ。
center = (left + right) / 2
ちなみに2回目は計算するとcenter = 1になる。

そして1回目と同じように中央の値と検索したい値を比較。
array[center] = 3
target = 5
なので、array[center] < targetとなる。

そして次の検索範囲が分かる。
SC3.png
今度は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メソッド使って比べていく方法もありますが、配列の中身が多いほどバイナリーサーチの方が便利ですね。
そういえば昔に友達との間で流行った某アプリってこの仕組みを使用してたのかな。

なんだか長くなってしまいました。最後までお付き合いありがとうございました。

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

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)

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

[rails] NOFILEのマイグレーションファイルを削除

事件内容

このようなファイルができてしまい、
rollbackができないため、マイグレーションファイルをdownさせることができない

up     20200926110535  ********** NO FILE **********

事件が起こった経緯

おそらくマイグレーションのステータスがUPの状態で
マイグレーションファイルを削除してしまった。

私の場合はgitデスクトップでchangeを丸ごと削除したので、この事件が起きた模様。

解決策

えっと、皆様が載せてくださっていた記事を何個か見させていただいた結果、無事解決いたしました。

まず、no faileに名前を付与するために
ターミナルでこのコマンドを実行してください:zap:
(マイグレーションIDはあなた様のnofileになっているIDに変えてください。
  その後の名前はダミーですのでなんでも構いません。)

% touch db/migrate/20200926110535_fuwafuwa.rb

実行後↓

20200926110535_fuwafuwa.rb
class Hoge < ActiveRecord::Migration[5.2]
  def change
  end
end

そうすれば、上記の名前マイグレーションファイルが
存在しているはずです。

バージョンはあなた様のrailsのバージョンを指定してください!!!!!:shamrock:

その後, rails db:migrate:statusで確認するとこのようになっていれば成功です。

up     20200926110535  fuwafuwa

あとは、先ほどのマイグレーションファイルを削除すれば無事解決!:corn:

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

しがない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を身に付けたい
    • 処理を見て非効率な部分を改善できるようになりたい
    • セキュアな書き方も身に付けたい
    • バックエンドに絞って頑張っていきたい


ざっと書いてみました。
最近感じたことしか覚えてないのでメモ書きです。

思い出した時に追記していきます。
自分用のメモ書きにはなりますが誰かのモチベや目標に繋がれば嬉しいです。

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

[Ruby on Rails]データ登録時に重複したレコードがあった場合、登録させない

背景

オリジナルアプリをデプロイ後、重複したデータを登録されていることに気づきます。
スクリーンショット 0002-09-27 午前9.43.35.png

アプリの仕様としては、登録するレコードは重複させたくないのです。

データベースを確認すると、
登録するユーザが異なっていれば、登録できてしまうことが判明。
スクリーンショット 0002-09-27 午前9.44.59.png

解決にそこそこ時間がかかったので、備忘として記録します。

問題箇所

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の記事が参考になり、解決に至りました。
:point_right:同じ名前のデータが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]) <= ココです。

以下、省略...

image.png

.
page_urlカラム内で検索をかけた結果、いくつあったか判定するために
countメソッドを追加します。

Company.where(page_url: "#{f}").count

取得したデータの数を条件にかけて1以上(データがあるか)判定する

1以上、つまり入力したデータと同じURLの数を数えて1以上あるか確認します。

Company.where(page_url: "#{f}").count >= 1

1以上(データがあり)ならば、受付けない

image.png

おまけ

アプリに反映させているので、是非遊んでみてください。
Unsung:hero
image.png

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

関連付しているモデルのレコードが消せずハマった

作業環境

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.rb
has_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_items
review.rb
belongs_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.rb
 create_table "comments", force: :cascade do |t|
   (省略) 
  end

commentsいた、、。不要なテーブルは消しましょう。

commentsテーブル削除

migrattionファイルを作成し、以下を記述して

def change
 drop_table :comments
end

rails db:migrateでさようなら。これでitem共にreviewの削除ができるようになりました!!

まとめ

たったこれだけで長々と書きましたが今回の件で得た教訓としては、機械は裏切らない、疑うのはまず自分であるということですね。思い込みはよくないです。
初投稿なので拙いところが多いですが、誰かの助けになればと思います。

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

【地図表示】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)カラムがあること

【参考】
Google MapsのAPIキーを取得する

過程

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.js
function 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>を参照しています。

結果

これで、ユーザーが登録した住所から投稿詳細ページに地図を表示できました!
200927_地図の表示.png

参考

Google Maps API の使い方・利用方法
Google Mapsを使ってみよう

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