20200928のRubyに関する記事は24件です。

【Ruby】リファクタリングに強くなるための基本のキ

 恥ずかしい話ですが、つい最近まで「綺麗なコードを書け」と言われると、条件反射的に「エラーがなけりゃいいじゃん。」と思っていました・・・。
 しかし、リファクタリングという言葉を理解しながら、いくつかの記法やメソッドに触れていくうちに、美しいコードを美しいと思えるようになってきた(すこし大袈裟:sweat_smile:)ので、基本的なリファクタリングの事例を紹介したいと思います。

今回設定するインプットとアウトプット

 1から5までの連続する配列の合計を出力せよ。(答えは15)
 これをいかに綺麗に記述して導き出すかに挑戦してみたいと思います。

その1:ザ・ごり押し

p 1 + 2 + 3 + 4 + 5

 …プログラムというか電卓?レベルの一番シンプルな記述。当然、1~100までの数字の合計を求める場合等に対応できないのでボツ。
拡張性を持たせていきたい。

その2:配列とeach文を使う

numbers = [1,2,3,4,5]
sum = 0 #変数sumに0を代入
 numbers.each do |n|
   sum += n #変数sumにブロック変数nを繰り返し足しこむ
 end
p sum

 each文で一番最初に習うコードかもしれない。プログラミングだけんども、「配列の要素を1,2,3…と書くのが面倒」、「each文まわりが数行にわたっててコンパクトにしたい」という気持ちが湧いてくる。

その3:範囲オブジェクトと{}のブロック記法でシンプルに

numbers = (1..5) #範囲オブジェクトの作成。1~5までの値が連続する配列を意味する。
sum = 0
  numbers.each {|n| sum += n } #{}のブロック記法で1行に収める。
p sum

 (最初の値..最後の値)で、値の範囲を表すことができる。
  ※範囲オブジェクトは、Rangeクラスのオブジェクト。
  ※ちなみに、(最初の値...最後の値)と書くと、最後の値を含まない。ドット.の数に注意
 また、each文のdo・・・endまでの間を、{}で代わりに囲むこともできる。
  ※do・・・endまでを「ブロック」と呼び、取り出した要素を扱う「作業部屋」のような役割。
 だいぶスッキリしました。けど、もう一息!

その4:eachの代わりにinjectメソッドを使う

numbers = (1..5)
 p sum = numbers.inject(0) {|n, s| n + s}
  #injectメソッドは下記の流れで動く。
  #メソッドの第1引数が、ブロックの第1引数に入る。
  #ブロックの第2引数には、配列の各要素が順番に入る。
  #ブロックの戻り値が、ブロックの第1引数に引き継がれる。
  #繰り返し処理が終わると、ブロックの戻り値がinjectメソッドの戻り値となる。

 injectメソッドを使うことで、引数を複数扱うことができ、2行に収めることができた。

 今後は、良著と聞いた「リーダブルコード」も読み進めながら、胸を張って見せられる綺麗なコードを書けるよう頑張っていきます。
 「こういう風に書いた方がもっと簡潔だよ。」という意見等ありましたら、ぜひ教えてください!

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

【Rails】ビューファイルは使用できない、パラメータIDも存在しない。そんな中でアソシエーションのデータを取得する。【APIモードで使用するやつ】

はじめに

備忘録です。

ここでは、ホーム画面("/")で

  • 「データ全件」
  • 「1つのデータに関連付けされているデータの総数」
  • 「1つのデータに関連付けされているハッシュのデータ」

を表示させたいとして、indexアクションにて上記のデータを取得する方法について見ていきます。
例えばQiitaのようなアプリケーションを作成するとするならば、

  • 「記事全件」
  • 「1つの記事に関連付けされているコメントの総数」
  • 「1つの記事に関連付けされているタグのハッシュデータ」

を取得していくようなイメージです。

全てのデータはハッシュとして取得します。

環境

Ruby: 2.6
Rails: 5.2

モデルの例(作成の過程は省略)

スクリーンショット 2020-09-28 23.12.40.png

コード

1.まずは記事全件を取得する

def index
  @all_article = Article.all
end

2.1つの記事に関連付けされているコメントの総数を取得する

def index
  @all_article = Article.all

  # [ { 1: "コメント数" }, { 2: "コメント数"  }, ... ]という形のデータを作成する。
  # article.commentsでループ処理した記事データからコメントを呼び出している。
  @article_count = @all_article.map{ |article| 
    [article.id, article.comments.count.to_s]  #もしコメント数を表示させるならto_sで文字列化する。
  }.to_h 

  # 上記のコメントデータのidと記事のidが一致した場合に、コメント数をcount属性に格納する
  @article_count.map do |key, value|
    @all_article.map do |article|
      if article.id === key
        article["count"] = value
      end
    end
  end

  # => [ { "id": 1, "count": "コメント数" }, { "id": 2, "コメント数" }, ... ]のように得られる
end

3.1つの記事に関連付けされているタグのハッシュデータを取得する(上と大体一緒)

def index
  # 記事全件
  @all_article = Article.all
  # コメント数
  @article_count = @all_article.map{ |article| 
    [article.id, article.comments.count.to_s]
  }.to_h 
  @article_count.map do |key, value|
    @all_article.map do |article|
      if article.id === key
        article["count"] = value
      end
    end
  end

  # 1つの記事が所有するタグをハッシュで取得
  @all_article.map{ |article|
    [article.id, article.article_tags.all]
  }.to_h

  # => { 
  #      "1": [
  #         { "id": 1, "tag_name": "Rails" }, 
  #         { "id": 2, "tag_name": "React" }, 
  #          ... ,
  #      ], 
  #      "2": [ 
  #         { "id": 1, "tag_name": "JavaScript" }, 
  #         { "id": 2, "tag_name": "Qiita" }, 
  #          ... ,
  #      ]
  #     }
end

以上ですが、上のままだとコントローラがごちゃごちゃしてしまうので、モデルファイルにインスタンスメソッドとして抽出するなどすると良いかと思います。


最後に

バックエンドとフロントエンドを切り離してSPAアプリケーションを作成する際にはビューファイルは使わないので、ルートのURLを持つホーム画面でアソシエーションされているデータを取得する方法を見つけるのにとても苦労しました。
で、結果このようにする結論に辿り着いたのですが、間違いやもっといい方法があるなどあれば、是非教えて頂けるととても助かります!?‍♂️

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

【Rails】ビューファイルは使わず、パラメータIDも存在しない中で、アソシエーションのデータを取得する。【APIモードで使う】

はじめに

備忘録です。

ここでは、ホーム画面("/")で

  • 「データ全件」
  • 「1つのデータに関連付けされているデータの総数」
  • 「1つのデータに関連付けされているハッシュのデータ」

を表示させたいとして、indexアクションにて上記のデータを取得する方法について見ていきます。
例えばQiitaのようなアプリケーションを作成するとするならば、

  • 「記事全件」
  • 「1つの記事に関連付けされているコメントの総数」
  • 「1つの記事に関連付けされているタグのハッシュデータ」

を取得していくようなイメージです。

全てのデータはハッシュとして取得します。

環境

Ruby: 2.6
Rails: 5.2

モデルの例(作成の過程は省略)

スクリーンショット 2020-09-28 23.12.40.png

コード

1.まずは記事全件を取得する

def index
  @all_article = Article.all
end

2.1つの記事に関連付けされているコメントの総数を取得する

def index
  @all_article = Article.all

  # [ { 1: "コメント数" }, { 2: "コメント数"  }, ... ]という形のデータを作成する。
  # article.commentsでループ処理した記事データからコメントを呼び出している。
  @article_count = @all_article.map{ |article| 
    [article.id, article.comments.count.to_s]  #もしコメント数を表示させるならto_sで文字列化する。
  }.to_h 

  # 上記のコメントデータのidと記事のidが一致した場合に、コメント数をcount属性に格納する
  @article_count.map do |key, value|
    @all_article.map do |article|
      if article.id === key
        article["count"] = value
      end
    end
  end

  # => [ { "id": 1, "count": "コメント数" }, { "id": 2, "コメント数" }, ... ]のように得られる
end

3.1つの記事に関連付けされているタグのハッシュデータを取得する(上と大体一緒)

def index
  # 記事全件
  @all_article = Article.all
  # コメント数
  @article_count = @all_article.map{ |article| 
    [article.id, article.comments.count.to_s]
  }.to_h 
  @article_count.map do |key, value|
    @all_article.map do |article|
      if article.id === key
        article["count"] = value
      end
    end
  end

  # 1つの記事が所有するタグをハッシュで取得
  @all_article.map{ |article|
    [article.id, article.article_tags.all]
  }.to_h

  # => { 
  #      "1": [
  #         { "id": 1, "tag_name": "Rails" }, 
  #         { "id": 2, "tag_name": "React" }, 
  #          ... ,
  #      ], 
  #      "2": [ 
  #         { "id": 1, "tag_name": "JavaScript" }, 
  #         { "id": 2, "tag_name": "Qiita" }, 
  #          ... ,
  #      ]
  #     }
end

以上ですが、上のままだとコントローラがごちゃごちゃしてしまうので、モデルファイルにインスタンスメソッドとして抽出するなどすると良いかと思います。


最後に

バックエンドとフロントエンドを切り離してSPAアプリケーションを作成する際にはビューファイルは使わないので、ルートのURLを持つホーム画面でアソシエーションされているデータを取得する方法を見つけるのにとても苦労しました。
で、結果このようにする結論に辿り着いたのですが、間違いやもっといい方法があるなどあれば、是非教えて頂けるととても助かります!?‍♂️

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

【No.003】発注者の注文一覧画面を作る

Issue
PR

概要

発注者の管理画面をつくる

ToDoリスト

  • Order::OrderingOrgSidesController追加
  • routes.rbをいい感じに
  • Orderモデル追加
  • Orderのレコードを作成する内容をseedに追加
  • Slimの導入
  • TailWindの導入
  • 見た目をFigmaに近づける

ToDo詳細

Order::OrderingOrgSidesController追加

ターミナルで下記をたたく。

bin/rails g controller order::ordering_sides

app/controllers/orders/ordering_org_sides_controller.rbを、下記に修正。

module Orders
  class OrderingOrgSidesController < ApplicationController

  end
end

routes.rbをいい感じに

config/routes.rbを以下のように。

Rails.application.routes.draw do
  root 'orders/ordering_org_sides#index'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  namespace :orders do
    resources :ordering_org_sides, only: %i[index]
  end
end

Orderモデル追加

図のOrderモデルを追加するために、ターミナルで下記をたたく

bin/rails g model Order trade_no:string title:string postal:string address:string name:string phone:string color_size:string status:integer

Orderのレコードを作成する内容をseedに追加

参考

db/seeds.rbに以下追加。

orders = Order.create(
  [
    {trade_no: '59466918', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 1},
    {trade_no: '56654093', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 2},
    {trade_no: '46263602', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 3},
    {trade_no: '76537895', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 4},
    {trade_no: '56939175', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 1},
    {trade_no: '83265169', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 2},
    {trade_no: '68545632', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 3},
    {trade_no: '86154160', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 4},
    {trade_no: '73779350', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 1},
    {trade_no: '16022030', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 2},
    {trade_no: '48758961', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 3},
    {trade_no: '94813841', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 4},
    {trade_no: '79330602', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 1},
  ]
)

ターミナルで以下を叩く。

rails db:seed

Slimの導入

一部、erbファイルを生成済みだったため、下記を参考に対応した。
https://qiita.com/rinkun/items/391ab7e8e63a7f20339c
Gemfileに下記を追加。

gem 'slim-rails'
gem 'html2slim'

ターミナルで、下記を打つ。

bundle exec erb2slim app/views app/views -d

Tailwindの導入

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'),
    // ...
  ]
}

見た目をFigmaに近づける

目標物

下図の見た目に近づける。

image
https://www.figma.com/proto/k7tWzvsQYtRHSwyi877OyV/import_agent_app?node-id=2%3A0&scaling=min-zoom

参考

Tailwind Tables サンプル

受入基準確認

準備

bin/rails db:migrate
bin/rails db:reset

受入基準

  • 下図のようになっている image
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

中二男子が一度は言ったことがある「無限ループ」

無限ループとは

eachメソッドやwhile文で、繰り返し処理をしたときに、終わりがなく、永遠に処理を繰り返している状態。もちろん、パソコンへの負荷がかかる。

具体例

number = 0
while number >= 0
 puts number
 number += 1
end

numberに1をたし続けて、出力していく。

while文

while 条件式
 #条件式が真であるときに繰り返す処理
end

if文のように、条件式が真でないと、処理は実行されない。trueと条件式に入れれば、確実に繰り返される。

無限ループを止める方法

breakを使う。
例えば、

number = 0
while number >= 0
 if number == 100
  break
 end
 puts number
 number += 1
end

if文を用いて、numberが100になったら、処理を終了させる。つまり、最後の出力は99になる。

ポイント

  • while文は条件式が真のときのみ、処理を行う。
  • 無限ルートはbreakで止められる。

最後に

無限ループになってしまう状況は中々ないのかなぁと思う。

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

[rails6.0.0]ウィザード形式でActiveStorageを使用して画像を保存する方法

概要

ユーザー登録とプロフィールをさせる時に、ウィザード形式でフォームを作成。
プロフィールに画像を保存したかったが上手くいかなかったため備忘録として記載。
※検索しても同じ状況の方がいなかったので役に立てばと思います。

ウィザード形式とは何か?という方はこちらを参考にしてください

内容

開発環境

MacOS Catalina 10.15.6
Rails 6.0.0
Ruby 2.6.5

テーブル構成

deviseを使ってユーザー登録をさせようとしています。
userテーブルはnicknameのみ追加。
profileテーブルにはtwitterのリンクなど記載。
profileテーブルにactive_strageで画像を保存したい。

ウィザード形式を実装

ここらへんの機能の実装に関しては
こちらの記事がかなり近いので参考にしました。

問題点

ウィザード形式で、画像以外はしっかり保存できたけど、画像は保存されない。

registrations_controller
class Users::RegistrationsController < Devise::RegistrationsController

  def create
    @user = User.new(sign_up_params)
    unless @user.valid?
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @profile = @user.build_profile
    render :new_profile
  end

  def create_profile
    @user = User.new(session["devise.regist_data"]["user"])
    @profile = Profile.new(profile_params)
    unless @profile.valid?
      render :new_profile
    end
    @user.build_profile(@profile.attributes)
    @user.save
    session["devise.regist_data"]["user"].clear
    sign_in(:user, @user)
    redirect_to root_path
  end

  private

  def profile_params
    params.require(:profile).permit(:avatar, :favorite_beer, :twitter_link, :info)
  end

この中の以下の記述が悪さをしていた様子。

registrations_controller
@user.build_profile(@profile.attributes)
@user.save

ここでは、@user@profileをbuildで関連付けさせて@user.saveでまとめて保存をしているのですが、ここでの保存が原因で上手く行ってない様子。
この記事を見つけて

原因
ActiveStorageに実際にファイルが保存されるタイミングはmodelをsaveして処理がコミットされた時なので、保存される前のmodelから画像をattachしてもファイルが未完成の状態になるっぽいです。

どうもActiveStrageはモデルをちゃんとsaveしないと行けなさそうなので以下のように記述を変更したら解決しました。

registrations_controller
# それぞれのモデルで保存させた    
# @user.build_profile(@profile.attributes)
@user.save
@profile.user_id = @user.id
@profile.save

まとめ

正直buildを使っての保存の仕組みがよくわかってなかったのに使用してしまったのがエラーの原因かもしれないです。
少しでも参考になれば幸いです。

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

[Ruby]sprintfで四桁のランダムな番号生成

目的

PINコードを生成したかったのでsprintfとrandメソッドを使った。

コード

sprintf('%04d', rand(10000)) //四桁に対して、余ったところを0で埋める 
=>"1184"

※ dは数字を意味する。(sならstring)

その他

sprintf('%10d', rand(10000)) //幅10の右づめ
sprintf('%-10d', rand(10000)) //幅10の左づめ
=> "      7179"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Markdown記法について

今回がQiita初投稿になります。よろしくおねがいします。

「プログラミング知識をMarkdown記法で書いて共有しよう」

と、Qiitaの投稿画面の一番初めにもデフォルトである通りQiitaにしろREADMEにしろGitHubのプルリクエストにしろプログラミング業界ではMarkdown記法というのがちょいちょい出てくるんですね。
自分も初めの頃は参考のサイトを見て少しやろうと思いましたが、案外やり方が多くて面倒になり別にまだ覚えなくてもいいかと思っていたのですが最近になり少しずつやり方が分かってきたので備忘録的な意味と自分の練習もかねて投稿しようと思いました。

まず初めにMarkdownについてですが

Markdownとは

Markdown(マークダウン)は文章を記述するための記法(マークアップ言語)の1つです。
Markdownとは、メールを記述する時のように書きやすくて読みやすいプレーンテキストをある程度見栄えのするHTML文書へ変換出来るフォーマットとしてジョン・グルーバーによって開発されました。
以下の特徴があります。

  • 簡単で覚えやすい記述
  • 文章の構造を明示出来る
  • Markdownそのままでも理解出来る
  • 対応アプリを使うことでより快適に読み書き出来る
  • 拡張子は「.md」

とのことです。

例として以下にRailsのアプリケーションのREADMEの一例を載せます。

README

users テーブル

Column Type Options
user_name string null: false
email string null: false
password string null: false

Association

  • has_many :friends
  • has_many :comments

というようにアプリケーションにはREADMEが必須でありその中の記述はMarkdown記法のためMarkdown記法を最低限は覚える必要があるということですね。

では自分がよく使うものを抜粋して紹介します。

  • 「#」を先頭に入れて半角スペース1つ分空けると見出しの意味になり大文字になり強調されます。(h1タグと同じ意味)
    「#」が一番大きくて以下1つずつ増やすごとに大文字がだんだんと小さくなり「######」最大6つまで連続で記述出来ます。

  • 「-」を先頭に入れて半角スペース1つ分空けると「・」が付与されます。

  • 「---」と「-」を3つ以上連続で入れると水平線が出来ます。

と上記が自分がよく使用している記述でした。上記の記述方法は本当にほんの一部なので興味のある人はちゃんとしたサイトから学習頂ければと思います。(笑)
と言ってもQiitaにいる人達はみんなMarkdown記法で記事を投稿しているのだから恐らくQiitaに投稿した人の中で自分が一番Markdown記法を知らない(使いこなしていない)と思いますのでこれから精進して参りたいと思います。ここまでお付き合い頂きありがとうございました。

注:Markdown記法の「#」や「-」などは全て半角で記述しないと正しく反映されません。さらには半角スペースも1つ分空けてください。全角では反映されませんので注意!

今回参考にさせて頂いたサイト

株式会社アーティス ビジネスとIT活用に役立つブログ

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

データベースのカラムなどを修正したい場合

db:migrateしたデータベースを修正したい場合

プログラミングスクールでのチーム開発学習中にデータベースの修正を行いたかったのですが、
細かい部分の知識が抜け落ちていたので、備忘録用にまとめてます。

データベースの操作に慣れていない方や、これからデータベースを学習する方の参考にもなれば幸いです。

開発環境

DB: MySQL
Rails: 5.2.4.3

まず自分が修正したいデータベース(テーブル)の確認を行いましょう

ターミナル
% rails db:migrate:status

すると、テーブルがこの様に出てくると思います(出てくるテーブルの数や名前はそれぞれ違ってきます)

ターミナル
Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200823051138  Devise create ----s
   up     20200824122031  Create -------s
   up     20200824122659  Add ancestry to ------s
   up     20200824123715  Create -----s
   up     20200829083145  Create -----s
   up     20200906141656  Create -----s 今回はこのテーブルを修正したい
   up     20200907114227  Create -----s
  down    20200927061950  Create -----s
  down    20200927065357  Create -----s
※----は自分で作成したテーブル名

ここでupとdownに注目です。

マイグレーションの修正を行うには、statusをdownの状態にしておく必要があります。

次に自分が修正したいデータベース(テーブル)をdownにしましょう

downの状態にするにはターミナルでこの様なコマンドを実行しましょう

ターミナル
% rails db:rollback

もう一度statusを確認してみましょう

ターミナル
% rails db:migrate:status
ターミナル
Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200823051138  Devise create ----s
   up     20200824122031  Create -------s
   up     20200824122659  Add ancestry to ------s
   up     20200824123715  Create -----s
   up     20200829083145  Create -----s
   up     20200906141656  Create -----s 今回はこのテーブルを修正したい
  down    20200907114227  Create -----s
  down    20200927061950  Create -----s
  down    20200927065357  Create -----s
※----は自分で作成したテーブル名

あれ?

一つ下しかdownに変わってません。

というのもrollbackコマンドは一つずつしかdownに変えられないのです。

なので、もう一度やってみましょう。

ターミナル
% rails db:rollback

もう一度statusを確認してみましょう

ターミナル
% rails db:migrate:status
ターミナル
Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200823051138  Devise create ----s
   up     20200824122031  Create -------s
   up     20200824122659  Add ancestry to ------s
   up     20200824123715  Create -----s
   up     20200829083145  Create -----s
  down    20200906141656  Create -----s 今回はこのテーブルを修正したい
  down    20200907114227  Create -----s
  down    20200927061950  Create -----s
  down    20200927065357  Create -----s
※----は自分で作成したテーブル名

今度は無事に目的のテーブルをdownに出来ました。

修正が終わった後

今回はカラム名の修正を行いたかったので、この後にマイグレーションファイルのカラム名の変更を行いました。

最後に

ターミナル
% rails db:migrate

もう一度statusを確認しておきましょう

ターミナル
% rails db:migrate:status
ターミナル
Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200823051138  Devise create ----s
   up     20200824122031  Create -------s
   up     20200824122659  Add ancestry to ------s
   up     20200824123715  Create -----s
   up     20200829083145  Create -----s
   up     20200906141656  Create -----s 修正したテーブル
   up     20200907114227  Create -----s
   up     20200927061950  Create -----s
   up    20200927065357  Create -----s
※----は自分で作成したテーブル名

rails db:migrateコマンドの場合は、downのテーブルを全てupに変更します。

db:migrateは1度で全てupにするけど
db:rollbackは1つずつしかdownに出来ないんですね。

一度にrollbackをまとめて行いたい場合

今回の様に複数回rollbackを行わないといけない場合にまとめて行える方法も紹介します

ターミナル
% rails db:rollback STEP=2

※STEP=2を入力する事でrollbackを2回分まとめて実行してくれます。

rollbackコマンドに慣れてきたら、STEPオプションも積極的に使って、作業性をあげていきましょう。

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

form_objectで親子関係のあるフォームを作成する(テストも書いてます)

背景

@shopに紐づく@comment@employeeなど、様々な子要素があるテーブルがあります。初回情報登録時には、@shop@comment@employeeなど、親要素と一緒に全ての子要素も一緒に保存できるようにしていたのですが、

下記の記事のような形で、accepts_nested_attributes_forを用いてこれらを実現していたものの、

▼こんなふうに実現していました
fields_forで子テーブルのデータを一気に作成する(テストも書いてます)[Rails][Rspec]

そのうち、@shopだけの編集フォームや、@comment@employeeなどの投稿・編集フォームも必要になってきたため、だんだんmodelが様々な記述で肥大化してきました。

form_objectとは?

↑上記のような状態の時に、特定のフォームに関するバリデーションやデフォルト値の設定などを一箇所に集め、モデルの記述を簡素化できるのが、form_objectです。個人的には、導入にかなりつまづいてしまったので、記事を書いて記録を残しておこうと思います。

なお、実行環境は下記の通りです。

  • Rails 5.2.4.2
  • rspec-rails 4.0.1

導入方法

form_object, controller, viewの基本の書き方は下記の通りです。なお、今回は@shopの初回登録時に@commentも1件登録できるようなフォームを例にしたいと思います。

実装にあたって、一番参考にさせていただいたのは、こちらの記事です。

accepts_nested_attributes_forを使わず、複数の子レコードを保存する

DB構造

shops
name string
category integer

↑ categoryはショップ種別。enumのカラム。

comments
content text
shop_id integer

作成したファイル

form_object

/forms/shop_entry_form.rb
class ShopEntryForm
  include ActiveModel::Model

  # @shopに関する記述 -----------------------------
  concerning :ShopBuilder do
    def initialize(params = {})
      super(params)
      @category = params[:category]
    end

    def facility
      @shop ||= Shop.new
    end
  end

  attr_accessor :name, :category
  validates :name, presence: true
  validates :category, presence: true

  # @commentに関する記述 -----------------------------
  concerning :CommentBuilder do
    attr_reader :comments_attributes

    def comments
      @comments_attributes ||= Comment.new
    end

    def comments_attributes=(attributes)
      @comments_attributes = Comment.new(attributes)
    end
  end

  attr_accessor :content

  # 実装のロジック ------------------------------------
  def save
    # バリデーションエラーならfalseを返して以下の処理は行わない
    return false if invalid?

    shop.assign_attributes(shop_params)
    build_asscociation

    shop.save ? true : false
  end

  private

  def shop_params
    {
      name: name,
      category: @category,
    }
  end

  def build_asscociations
    # shopの子要素にcommentを追加する。ただし、中身が空なら追加しない。
    shop.comments << comments if comments[:content].present?
  end


end

これだけでつまづきどころがかなりありました。。。。
まず、concerning :ShopBuilder do ... endの部分ですが、以下のような意味を持ちます。

# この記述は...
concern :ShopBuilder do
  ...
end

# 下記と同じ
module ShopBuilder
  extend ActiveSupport::Concern
  ...
end

詳しくは、実装にあたって参考にした、こちらの記事をご覧ください。

次に、initialize(params = {}) ... endの部分なのですが、以下のような意味を持ちます。

def initialize(params = {})
  # @shopのparamsにアクセスできるようにする
  super(params)

  # DBでデフォルト値が設定されているカラム用の記述
  @category = params[:category]
end

まず、super(params)については、こちらも実装にあたって大変参考にさせていただいた記事である以下の記事によると

フォームクラスを使う

super(params)でパラメーターを格納する記述で、以下の記述と同じ意味を持ちます。

@attributes = self.class._default_attributes.deep_dup
assign_attributes(params)

また、db側でデフォルト値が設定されているカラムは、以下のように明示的にparamsにアクセスすることを書かないとparamsにアクセスできず、値を入力してもDBのデフォルト値になってしまいました...。

@category = params[:category]

この謎は解けず。今後の課題としたいです。。。
enumを使ったカラムにdb側でデフォルト値が必要な理由は、こちらの記事をご覧ください。

そして def comments_attributes=(attributes) ... end の部分なのですが、

def comments_attributes=(attributes)
  @comments_attributes = Comment.new(attributes)
end

こちらはRailsばかりやっているとなかなか目にしない、セッターメソッド という書き方で、=でおわるメソッド(引数)の形で、引数によって@のつく要素を変更することができます。
個人的には、こんなことをやっているイメージに近いのではないかなと思いました。

def comments_attributes=(attributes) # ... 以下略

# こんなイメージ
comments_attributes = attributes

# なので、こんな感じに呼び出せる
self.comments_attributes
# => attributesの中身

Rubyのゲッターとセッターを正しく理解していなかったせいですね。。。。トホホ。。。頑張ります。。。
なお、=でおわるメソッドについては、『プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで』p215を15回ぐらい読み直しました。

controller

次は、コントローラーの記述です。コントローラーはこのような形になりました。

app/controllers/shops_controller.rb
class ShopsController < ApplicationController

  def new
    @shop = ShopEntryForm.new
  end

  def create
    @shop = ShopEntryForm.new(shop_entry_params)
    if @shop.save
      # 成功したときの処理
    else
      # 失敗したときの処理
    end
  end

  private

  def shop_entry_params
    params.require(:shop_entry_form).permit(:caregory,
                                            :name,
                                            comments_attributes: [:content])
  end
end

こちらは、意外に記述が減らなかった印象があります。当初shop_entry_paramsがcontrollerから減ってくれればいいなーと期待したものの、結局controllerからは消せず。アソシエーションを作るメソッドだけはcontrollerから削除することができました。

なお、Modelに関しては、バリデーションとデフォルト値設定のメソッド、アソシエーションなども全て消すことができました!増えた記述は、なし!!やはり、form_objectはモデルをスリム化するために便利な書き方なのですね!!

View

最後に、Viewはこのようになっています。

app/views/shops/new.html.haml
= form_with model: @shop, url: shops_path, local: true do |f|
    = f.text_field :name

    = f.fields_for :shop_comments, local: true do |comment_form|
      = comment_form.text_field

    = f.submit "送信"

fields_forを使うあたりは、accept_nested_attributes_forを使った実装と変わらないのですね^^

テスト

テストも至ってシンプルでした!

spec/forms/shop_entry_form_spec.rb
require 'rails_helper'

RSpec.describe ShopEntryForm, type: :model do
  before do
    @shop_form = ShopEntryForm.new(category: "category1", name: "テストのお店")
  end

  describe "バリデーションのテスト" do
    it "名前とカテゴリーがあればバリデーションを通過すること" do
      @shop_form.valid?
      expect(@shop_form).to be_valid
    end

    # 以下略
  end
end

ファイルの置き場所と、RSpec.describe ShopEntryForm ...の部分, テスト用のインスタンス生成時の記述に注意すれば良いだけでした^^

これは、少し古いのですがこちらの記事を参考に作成しました。

フォームオブジェクトのテストをRSpecで書く

感想・参考資料など

さて、、、、本当に長い時間が実装にかかりました。実際のフォームはネストした子要素が3種類もあったり、形もかなり複雑だったのもあるのですが、何よりも素のRubyの書き方に慣れていなかったのが大きかったと思います。。。落ち着いたら、またRubyを復習したいです。

今回、参考にした記事や資料まとめです。

▼全体的な書き方
accepts_nested_attributes_forを使わず、複数の子レコードを保存する

▼paramsへのアクセス方法
フォームクラスを使う
『プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで』(p.215)

▼Concerningについて
Bite-sized separation of concerns

▼テストの書き方
フォームオブジェクトのテストをRSpecで書く

この後、editとupdateのフォームも残っているので、次はそちらを取り組みたいです^^

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

「もっとRust脳を鍛える数学パズル」の試み。

「もっとプログラマ脳を鍛える数学パズル」をRustで書き直すのは、ボケ防止にちょうど良いかもしれない、と思った。

P.13 例題1 : メモ化と動的計画法

元のRubyコード(p.015)。

pre1_2.rb
M, N = 10, 100

@memo = {}
def check(remain, pre)
    return @memo[[remain, pre]] if @memo[[remain,pre]]

    return 0 if remain < 0
    return 1 if remain == 0

    cnt = 0
    pre.upto(M) do |i|
        cnt += check(remain - i, i)
    end
    @memo[[remain, pre]] = cnt
end

puts check(N, 2)

Rustベタ移植。

main.rs
use std::collections::HashMap;

fn main() {
    let mut memo: HashMap<(i64, i64), i64> = HashMap::new();
    println!("{}", check(&mut memo, 10, 100, 2));
}

fn check(
    memo: &mut HashMap<(i64, i64), i64>,
    max_seat_at_one_table: i64,
    remain: i64,
    pre: i64,
) -> i64 {
    match memo.get(&(remain, pre)) {
        Some(cnt) => return *cnt,
        _ => {
            if remain < 0 {
                return 0;
            } else if remain == 0 {
                return 1;
            } else {
                let mut count = 0;
                for i in pre..=max_seat_at_one_table {
                    count += check(memo, max_seat_at_one_table, remain - i, i);
                }
                memo.insert((remain, pre), count);
                return count;
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_check() {
        let max_seat_at_one_table = 10;
        let number_of_people = 100;
        let mut memo: HashMap<(i64, i64), i64> = HashMap::new();
        assert_eq!(
            check(&mut memo, max_seat_at_one_table, number_of_people, 2),
            437_420
        );
    }
}

Rubyのuptoと同様の動きをRustのforにやらせるには、+1しないといけない。
と思ってたら @scivola さんからご指摘。ありがとうございます!

Rubyのショートコードでは何とも思わないグローバル変数だが、さすがにRustでやると気持ち悪い、というかやり方が分からないので、持ち回っている。Amazonの書評にもあったけど、変数を数学っぽく省略すると訳が分からなくなるので、ひどいところだけ、何となく長い名前にした。

改めて、条件分岐の処理にRubyの気楽さ(=プログラマへの信頼)を感じる。Rustはコンパクトながら抜けを許さないところが、これはこれで良い感じ。

2020/09/29追記

グローバル変数の扱いの指摘を受けて再度実装。グローバル変数をstructとして実現する案。これはもうデザインパターンに片足突っ込んでますね。

main.rs
use std::collections::HashMap;

struct Checker {
    memo: HashMap<(i64, i64), i64>,
    max_seat_at_one_table: i64,
}

impl Checker {
    pub fn check(&mut self, remain: i64, pre: i64) -> i64 {
        match &self.memo.get(&(remain, pre)) {
            Some(cnt) => return **cnt,
            _ => {
                if remain < 0 {
                    return 0;
                } else if remain == 0 {
                    return 1;
                } else {
                    let mut count = 0;
                    for i in pre..=self.max_seat_at_one_table {
                        count += self.check(remain - i, i);
                    }
                    &self.memo.insert((remain, pre), count);
                    return count;
                }
            }
        }
    }
}

fn main() {
    let mut chk = Checker {
        memo: HashMap::new(),
        max_seat_at_one_table: 10,
    };
    println!("{}", chk.check(100, 2));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_check() {
        let number_of_people = 100;
        let mut chk = Checker {
            memo: HashMap::new(),
            max_seat_at_one_table: 10,
        };
        assert_eq!(chk.check(number_of_people, 2), 437_420);
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】 binding.pryの活用方法

この記事では、binding.pryの使い方を解説しています。
 
binding.pryを活用することで、
・一次ソースに触れる機会が増え、学習効率が上がる
・binding.pryを複数設置して、paramsの流れが理解しやすくなる

など、たくさんメリットがあります。
 
自分みたいな、初学者方の参考になればと思い、記事にしてみました。

前提

チャットアプリを題材にbinding,pryの使い方を学びます。
(注意:この記事ではチャットアプリは完成しません!チャットアプリ作成の記事ではありません)

開発環境
・ruby 2.6.5
・Rails 6.0.3.3

完成イメージ

chat_demo.gif

ER図

名称未設定ファイル (1).png

必要なテーブル
・usersテーブル
・roomsテーブル
・entriesテーブル (中間テーブルです!)

流れ

①user, room, entryモデル、テーブルを作成(下準備)

②アソシエーションを書く(下準備)

③Gemfileにpry-railsを追加してbundle installを実行(下準備)

④ビューにform_withを用意

⑤roomsコントローラーにcreateアクションを書く

①user, room, entryモデル、テーブルを作成(下準備)

userモデルのマイグレーションを編集
userモデルは、deviseを使って作成しているものとします!

db/migrate/xxxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :nickname,  null: false
      t.string :email,     null: false, default: ""
      #省略
    end
    #省略
  end
end

usersテーブルには、nicknameとemailのカラムを用意しています。

roomモデルのマイグレーションを編集

db/migrate/xxxx_create_rooms.rb
class CreateRooms < ActiveRecord::Migration[6.0]
  def change
    create_table :rooms do |t|
      t.string :name, null: false
      t.timestamps
    end
  end
end

roomsテーブルには、nameのカラムを用意しています。

entryモデルのマイグレーションを編集(下準備)

db/migrate/xxxx_create_entries.rb
class CreateEntries < ActiveRecord::Migration[6.0]
  def change
    create_table :entries do |t|
      t.references :room, foreign_key: true
      t.references :user, foreign_key: true
      t.timestamps
    end
  end
end

entriesテーブルは、usersテーブルとroomsテーブルを繋ぐ中間テーブルなので、
user, roomそれぞれを外部キーとして、references型で保存するようにしています。

②アソシエーションを書く(下準備)

一人のuserは複数のroomに入れて、
一つのroomは複数人のuserが入るので、
usersテーブルとroomsテーブルは「多対多」の関係になります。
以下のように、アソシエーションを記述します。

userモデル

app/models/user.rb
class User < ApplicationRecord
  #省略
  has_many :entries
  has_many :rooms, through: :entries
end

roomモデル

app/models/room.rb
class Room < ApplicationRecord
  has_many :entries
  has_many :users, through: :entries
end

entryモデル

app/models/entry.rb
class Entry < ApplicationRecord
  belongs_to :room
  belongs_to :user
end

③gem 'pry-rails' をインストール(下準備)

pry-railsをインストールできるようGemfileに記述して、bundle installを実行します。
参考 : rweng/pry-rails: Rails >= 3 pry initializer - GitHub

Gemfile
gem 'pry-rails'
ターミナル
% bundle install

④ビューにform_withを用意

新しくルームを作成するために、roomsコントローラーにnewアクションを定義します。

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def new
    @room = Room.new #newメソッドでインスタンスを作成
    @users = User.all #全ユーザーのレコードを取得
  end
end

 
ビューには、フォームを設置します。
チャットルーム名(name)を入力し、チャットしたい相手(user_ids)を選べるようにします。

app/views/rooms/new.html.erb
<%= form_with model: @room, local: true do |f| %>
  <%= f.label :チャットルーム名%>
  <%= f.text_field :name%>            <%# 入力したチャットルーム名を取得 %>
  <label>チャットしたい相手</label>
  <select name="room[user_ids][]">        <%# 選択したユーザーを取得 %>
    <option value="">未選択</option>
    <% @users.each do |user| %>
      <option value=<%= user.id %>><%= user %></option>
    <% end %>
  </select>
  <%= f.submit %>
<% end %>

ここで、user_idsと複数形になっているのは、自分と相手の2人分保存するからです!
userモデルにて、has_manyを定義したことで、_idsメソッドが使えるようになりました!
参考 : Active Record の関連付け - Railsガイド

実際にフォームへ入力してみます。
すると下記のように、ユーザーが誰が誰だか分からないではありませんか!

ユーザー一覧表示_失敗

では、何が原因でこの出力が得られたのか推測します。

考えやすくするために、ユーザー選択の記述を、rubyの文法で書き直してみます。

@users.each do |user| 
 user 
end 

@usersは、roomsコントローラーのnewアクションで定義しているインスタンス変数で、
@users = User.allと定義しています。
ビューの中では、each文による繰り返し処理によって、@usersから一人ずつ取り出しています。

では、binding.pryを使って、出力される値を確認してみましょう。

app/views/rooms/new.html.erb
<%= form_with model: @room, local: true do |f| %>
  <%= f.label :チャットルーム名%>
  <%= f.text_field :name%>            <%# 入力したチャットルーム名を取得 %>
  <label>チャットしたい相手</label>
  <select name="room[user_ids][]">      <%# 選択したユーザーを取得 %>
    <option value="">未選択</option>
    <% @users.each do |user| %>
      <% binding.pry %>                  <%# ?each文の中にbinding.pryを設置!! %>
      <option value=<%= user.id %>><%= user %></option>
    <% end %>
  </select>
  <%= f.submit %>
<% end %>

 
ブラウザをリロードすると、ターミナルに以下のような出力が表示されます。

ターミナル
     3:   <%= f.text_field :name%>
     4:   <p><label>チャットしたい相手</label></p>
     5:   <select name="room[user_ids][]">
     6:     <option value="">未選択</option>
     7:     <% @users.each do |user| %>
 =>  8:     <% binding.pry %>
     9:       <option value=<%=user.id%>><%= user %></option>
    10:     <% end %>
    11:   </select>
    13:   <p><%= f.submit%></p>

[1] pry(#<#<Class:xxxx>>)> 

=>で、ビューの8行目で処理を止めてるよ!とターミナルが教えてくれています。
 
7〜10行間は、each文で繰り返し処理していることから、
繰り返し処理の1回目で、一時的に処理を止めてくれています。

したがって、userには一人目のデータが格納されていると考えられます。
では、実際にuserの値を確認してみましょう。

ターミナル
[1] pry(#<#<Class:xxxx>>)> user
=> #<User id: 1, nickname: "user_1", email: "test@1">
[2] pry(#<#<Class:xxxx>>)> user.nickname
=> "user_1"

[1]pry> の後に、を入力することで、
=>後に、を出力してくれます。

userには、idが1であるユーザーのレコードが格納されていることが確認できました。
 
今回はユーザー名を一覧表示させたいので、nicknameの値だけを取り出すことにします。
2回目のpryで、user.nicknameと記述すると、ユーザー名を取り出せると確認できました。
したがって、ビューファイルを下記のように書き換えます。

app/views/rooms/new.html.erb
<%= form_with model: @room, local: true do |f| %>
  <%= f.label :チャットルーム名%>
  <%= f.text_field :name%>            <%# 入力したチャットルーム名を取得 %>
  <label>チャットしたい相手</label>
  <select name="room[user_ids][]">      <%# 選択したユーザーを取得 %>
    <option value="">未選択</option>
    <% @users.each do |user| %>
      <option value=<%= user.id %>><%= user.nickname %></option> <%# ?user.nicknameを表示するように変更!! %>
    <% end %>
  </select>
  <%= f.submit %>
<% end %>

ユーザー一覧表示_成功

無事にユーザー名を一覧表示することができました!
 
しかし、この一覧表示には、一つだけ問題があります。
それは、自分自身も表示されていることです。
 
このままでは、自分しかいない孤独なチャットルームが作成されてしまいます...。
このような事態を防ぐために、自分以外のユーザーを一覧表示するようにします。

どんな式が必要か、binding.pryを活用して探していきます。

ターミナル
     3:   <%= f.text_field :name%>
     4:   <p><label>チャットしたい相手</label></p>
     5:   <select name="room[user_ids][]">
     6:     <option value="">未選択</option>
     7:     <% @users.each do |user| %>
 =>  8:     <% binding.pry %>
     9:       <option value=<%= user.id %>><%= user.nickname %></option>
    10:     <% end %>
    11:   </select>
    12:   <p><%= f.submit%></p>

[1] pry(#<#<Class:xxxx>>)>

まず初めに、@usersの中身を確認しましょう。

ターミナル
[1] pry(#<#<Class:xxxx>>)> @users
=> [#<User id: 1, nickname: "user_1", email: "test@1">,
 #<User id: 2, nickname: "user_2", email: "test@2">,
 #<User id: 3, nickname: "user_3", email: "test@3">,
 #<User id: 4, nickname: "user_4", email: "test@4">,
 #<User id: 5, nickname: "user_5", email: "test@5">] # ?現在のユーザー(current_user)

@usersには、全ユーザーのデータが、一人ずつ配列で格納されていることが確認できます。
ではそもそも、インスタンス変数@usersとは何と定義していたかというと、
User.allと等しいよ!と定義していましたね。
 
では、User.allの内容を確認しましょう。

ターミナル
[2] pry(#<#<Class:xxxx>>)> User.all
=> [#<User id: 1, nickname: "user_1", email: "test@1">,
 #<User id: 2, nickname: "user_2", email: "test@2">,
 #<User id: 3, nickname: "user_3", email: "test@3">,
 #<User id: 4, nickname: "user_4", email: "test@4">,
 #<User id: 5, nickname: "user_5", email: "test@5">]

@usersと全く同じデータが出力されることが確認できました。

続いて、現在のユーザーのデータを取り出せないか試してみます。

ターミナル
[3] pry(#<#<Class:xxxx>>)> User.all.where(id: current_user)
=> [#<User id: 5, nickname: "user_5", email: "test@5">]

.whereはActive Recordのメソッドの一つで、条件に該当するレコードを配列に格納して出力してくれます。
超便利なのでどんどん使っていきましょう。
参考 : Active Record の基礎 - Railsガイド

さて、現在のユーザーのデータを取り出すことができました。

ということは、現在のユーザー以外のデータも取り出せるのでは?と思いつきます。
.where.notメソッドを使うと良さそうです。
.where.notは、条件に該当しないレコードを配列に格納して出力してくれる、.whereと対をなすメソッドです。

ターミナル
[4] pry(#<#<Class:xxxx>>)> User.all.where.not(id: current_user)
=> [#<User id: 1, nickname: "user_1", email: "test@1">,
 #<User id: 2, nickname: "user_2", email: "test@2">,
 #<User id: 3, nickname: "user_3", email: "test@3">,
 #<User id: 4, nickname: "user_4", email: "test@4">]

現在のユーザーはuser_5なので、現在のユーザー以外のデータが出力されています。
 
これで、現在のユーザー以外を一覧表示させる式を見つけることができました!
したがって、@usersの定義を変更しましょう。

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def new
    @room = Room.new #newメソッドでインスタンスを作成
    @users = User.all.where.not(id: current_user) #現在のユーザー以外のレコードを取得
  end
end

ユーザー一覧表示_自分以外_成功
これで自分だけのチャットルームを作らないよう設定できました。
 
めでたしめでたし...と言いたいところですが、
roomを保存できるか確認してみましょう。

⑤roomsコントローラーにcreateアクションを書く

roomsコントローラーにcreateアクションを定義します。

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def new
    @room = Room.new
    @users = User.all.where.not(id: current_user)
  end

  def create      #createアクションを定義
    binding.pry
  end
end

フォームで入力した情報(リクエスト)を確認したいので、
この段階では、createアクションには何も処理は定義せず、
binding.pryを設置しておきます。
 
こうすることで、
「フォームで入力された情報が届いたよー!」
と、ルーティングを介して、roomsコントローラーのcreateアクションを実行する瞬間に、
処理を止めることができます。
 
では、フォームにルーム名room1、チャットしたい相手user_2と選択して送信します。
すると下記のようにターミナル上で、createアクション内で定義したbinding.pryで処理を止めてるよと教えてくれます。

ターミナル
    7: def create
 => 8:   binding.pry
    9: end

[1] pry(#<RoomsController>)> 

 
では、リクエストのパラメータを確認してみましょう。

ターミナル
[1] pry(#<RoomsController>)> params
=> <ActionController::Parameters {"authenticity_token"=>"xxxxxxx==", "room"=>{"name"=>"room1", "user_ids"=>["2"]}, "commit"=>"Create Room", "controller"=>"rooms", "action"=>"create"} permitted: false>

params(パラムス)はパラメーターズの略です。
authenticity_tokenは、セキュリティのために生成されるトークンなので、今回は無視します。
 
roomの中に、form_withで入力したパラメータが、配列としてハッシュで管理されています。
このroomとは、form_withで用意した、model: @roomと対応しています。

では、paramsの中の、roomの情報だけ見てみます。

ターミナル
[2] pry(#<RoomsController>)> params[:room]
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2"]} permitted: false>

params[:xxxx]とすることで、見たいパラメータxxxxだけを確認できます。
上記の結果から、チャットルームroom1user_idが2のユーザーが入ったことが確認できます。
ちゃんとルームに人を呼べていることが確認できましたね。
 
めでたしめでたし.....
 
って、自分自身がルームに入ってないじゃん!!!
 
ここから、自分もルームに入れるよう、ビューを書き換えきます!

app/views/rooms/new.html.erb
<%= form_with model: @room, local: true do |f| %>
  <%= f.label :チャットルーム名%>
  <%= f.text_field :name%>            <%# 入力したチャットルーム名を取得 %>
  <label>チャットしたい相手</label>
<select name="room[user_ids][]">        <%# 選択したユーザーを取得 %>
    <option value="">未選択</option>
    <% @users.each do |user| %>
      <option value=<%= user.id %>><%= user.nickname %></option>
    <% end %>
  </select>
  <input name="room[user_ids][]" type="hidden" value=<%=current_user.id%>>  <%# ?現在のユーザーもroomに追加するように変更!! %>
  <%= f.submit %>
<% end %>

inputは、formにおけるテキストフィールドの種類を指定します。
hidden属性を指定することで、ブラウザには表示せずにパラメータとしてデータを受け渡すことができます。
この記述では、user_idscurrent_user(現在のユーザー)も含まれるよう記述しています!
 
ではもう一度、フォームにルーム名room1、チャットしたい相手user_2と選択して送信します。

ターミナル
[1] pry(#<RoomsController>)> params
=> <ActionController::Parameters {"authenticity_token"=>"xxxxxxx==", "room"=>{"name"=>"room1", "user_ids"=>["2", "5"]}, "commit"=>"Create Room", "controller"=>"rooms", "action"=>"create"} permitted: false>

[2] pry(#<RoomsController>)> params[:room]
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: false>

無事に現在のユーザー(user_5)user_idsに含まれていることが確認できました!
 
続いて、createアクションを定義し直して、テーブルにデータが保存できるようにしましょう。

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  #省略
  def create
    @room = Room.new(room_strong_params)
    if @room.save
      redirect_to root_path
    else
      render :new
    end
  end

  private

  def room_strong_params
    params.require(:room).permit(:name, user_ids: [])
  end
end

ストロングパラメータは、roomモデルの、nameuser_idsのパラメータだけ許可するとしています。
 
createアクションの内部で、どのようにパラメータの受け渡しがされているか、binding.pryを使って確認しましょう。

app/controllers/rooms_controller.rb
  #省略
  def create
    @room = Room.new(room_strong_params)
    binding.pry      # ?binding.pryを設置!!
    if @room.save
      binding.pry    # ?binding.pryを設置!!
      redirect_to root_path
    else
      render :new
    end
  end

インスタンス変数@roomが、保存される前、後でパラメータをそれぞれ確認してみます。
 
では、フォームにルーム名room1、チャットしたい相手user_2と選択して送信します。

ターミナル
     7: def create
     8:   @room = Room.new(room_strong_params)
 =>  9:   binding.pry
    10:   if @room.save
    11:     binding.pry
    12:     redirect_to root_path
    13:   else
    14:     render :new
    15:   end
    16: end

[1] pry(#<RoomsController>)>

@room保存前の各パラメータを確認します。

ターミナル
[1] pry(#<RoomsController>)> params[:room]
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: false>

[2] pry(#<RoomsController>)> @room
=> #<Room:xxxx id: nil, name: "room1", created_at: nil, updated_at: nil>

[3] pry(#<RoomsController>)> room_strong_params
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: true>

2回目のpryに注目してください。
@roomは、id: nilであることから、この時点では、レコードは作成されていないと分かります。
 
3回目のpryでは、ストロングパラメータを確認していますが、nameuser_idsに値が正しく格納されていることが確認できます。
 
リクエストしたパラメータが、コントローラーのcreateアクションに正しく受け渡されているのに、まだレコードが作成されていない理由は、
.newメソッドでインスタンスを作成する場合、
.saveメソッドを実行して初めてデータベースにレコードとしてコミットされるからです。

では、@room.save後を確認してみます。

ターミナル
     7: def create
     8:   @room = Room.new(room_strong_params)
     9:   binding.pry
    10:   if @room.save
 => 11:     binding.pry
    12:     redirect_to root_path
    13:   else
    14:     render :new
    15:   end
    16: end

[1] pry(#<RoomsController>)> params[:room]
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: false>

[2] pry(#<RoomsController>)> @room
=> #<Room:xxxx id: 1, name: "room1">

[3] pry(#<RoomsController>)> room_strong_params
=> <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: true>

2回目のpryに注目してください。
@roomは、id: 1であることから、レコードは正常に保存されました!

今回は無事に保存できたのですが、
レコードを保存できなかった時に使える便利なメソッドも、合わせて紹介します!

ターミナル
[4] pry(#<RoomsController>)> @room.valid?
=> true

[5] pry(#<RoomsController>)> @room.errors
=> #<ActiveModel::Errors:xxxx @base=#<Room:xxxx id: 1, name: "room1">, @details={}, @messages={}>

[4] pryの@room.valid?では、「@roomのバリデーションはOK?」みたいな感じで、
バリデーションを実行してエラーがあるかを判別します。
エラーが無ければtrueを,
エラーが有ればfalseを返します。

[5] pryの@room.errorsでは、@room.valid?でfalseが返された時に、エラーメッセージを出力してくれます。
今回はエラーはないので、エラーメッセージは出力されていません。
エラーがある時はmessages{}の中にエラーメッセージが格納されます。

最後に、保存されたデータをコンソールで確認してみましょう。

ターミナル
% rails c
[1] pry(main)> Room.all
=> [#<Room:xxxx id: 1, name: "room1">]

[2] pry(main)> Entry.all
=> [#<Entry:xxxx id: 1, room_id: 1, user_id: 2>,
 #<Entry:xxxx id: 2, room_id: 1, user_id: 5>]

コンソールでも、Active Recordのメソッドを使うことができます。
 
Room.allで全てのルームを表示させると、
room1が保存されていることが確認できます。

Entry.allで全てのレコードを表示させると、
2つのレコードが保存されていることが確認できます。
「ルーム1にユーザー2と5がいるよーっ!」と教えてくれています。

コンソールで確認した内容は、以下の表と同じ内容です!

roomsテーブル

id name
1 room1

entriesテーブル

id room_id user_id
1 1 2
2 1 5

最後までお付き合いいただきありがとうございました!:smile:

参考資料

【Rails】find・find_by・whereについてまとめてみた

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

rails5.2.4 で作成した既存のアプリを5.1.6にダウングレードする

rails5.2.4で開発を進めていたのですが、既存のサービスとバージョンを合わせて管理がしたかったため、ダウングレードを選択しました。
環境はdockerで構築しているため、 ローカルの場合は使用しているPC等のrailsのバージョン変更が必要になります。

手順

1.Gemfileのrailsのバージョンを書き換える

Gemfile
gem "rails", "5.1.6"

2.bundle update (dockerのサービス名はwebにしています)

$ docker-compose run web bundle update

3.configの書き換え

application.rbのconfig.load_defaultsが5.2になっているので、5.1に変更

application.rb
 config.load_defaults 5.1

4.Active Recordの設定解除

application.jsのactive_storageの行を削除

application.js
//=require active_storage

configのactive_storageの行を削除

production.rb
config.active_storage.service = :local
development.rb
config.active_storage.service = :local

5.secrets.ymlを作成

config配下にsecrets.ymlを作成し、bundle exec rake secretで鍵の作成をする。

参考記事
https://qiita.com/tanishilove/items/2801059830e5af1262d7

6.最後にdevelopment.rbに残っているActive_storageの設定を削除

config.active_record.verbose_query_logsをfalseに変更

development.rb
config.active_record.verbose_query_logs = false

最後に

バージョンを下げる機会はなかなかないかと思いますが、困った時に参考になればと。

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

【Rails】検索フォームで、ひらがな・カタカナ・漢字の区別なく検索(精度は100%ではないよ)

自分がPFとして作っているアプリに検索フォームを実装しました。
ただ、【rails 検索 フォーム】 とかで検索すると、部分一致や完全一致が多く出てきます。
なんとかして漢字の部分をひらがなやカタカナで検索できないかな、と思いやってみました。

流れとしては、動画のタイトルを保存する時に、そのタイトルをローマ字に変換して専用のカラムに保存し、検索するときも検索ワードをローマ字に変換して専用のカラムと参照する、という感じです。
最初は検索する時にタイトルを全部変換しようかと思いましたが、動画が増えると時間がかかりそうだな・・・と思ったので上記の方法にしました。

如何せん初学者なので、そんな冗長なことしなくてもみたいな部分はあるのと思いますが、忘備録の意味合いも込めて書くので大目に見てやって下さい。

参考にしたサイト

検索フォームを作る

検索フォーム自体は色々と記事があるので簡単に作れると思います。
自分の場合は動画の投稿サイトです。検索で、検索ワードが動画のタイトルに一致する、という検索フォームを作ります。

/application.html.erb
      <div id="search-box">
        <%= form_tag(search_path, :method => 'get') do %>
          <div class="input-tag">
            <%= text_field_tag :search, '', placeholder: '検索', value: params[:title] %>
          </div>
          <div class="submit tag">
            <%= button_tag type: 'submit', class: 'btn btn-default' do %>
              <i class="fas fa-search"></i>
            <% end %>
          </div>
        <% end %>
      </div>

ルーティングを、videosコントローラーのサーチアクションに飛ばします。

routes.rb
  # 検索機能
  get "search" => "videos#search"

コントローラーに追記していく

gemを導入します。
今回、 miyabi というgemを使いました。ひらがな〜カタカナ〜ローマ字に変換したり判定したりできるgemです。
今回使ったメソッド

.to_roman   #文字列をローマ字に変換
.to_kanhira #漢字が含まれた文字列をひらがなに変換
.is_hira?   #文字列がひらがなか判定
.is_kana?   #文字列がカタカナか判定

gemを導入したらコントローラーのcreateとsearchを書いていきます。
Videoというモデルには
user_id title introduction
のカラムがあり、そこにタイトルをローマ字に変換した物を保存する conversion_title というカラムを追加しました。

schema.rb
  create_table "videos", force: :cascade do |t|
    t.integer "user_id"
    t.string "title"
    t.text "introduction"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "conversion_title"
  end

videos.controllerのcreateを作るのですが、問題が発生しました。

gem 'miyabi' では、【漢字が含まれている文字列を変換】はできるのですが、【漢字が文字列に含まれているか】は判定することができません。

rubyの持つ、正規表現で漢字が含まれているかどうかを判断します。
今回は漢字がタイトルに含まれているかどうかを調べたいので、ピンポイントで漢字だけを判定させます。

@video.title.match(/[一-龠々]/)

これで漢字が含まれているかどうか判定できます。

createで投稿された動画を保存します。

/videos_controller.rb
  def create
    @video = Video.new(video_params)
    @video.user_id = current_user.id
    if @video.title.match(/[一-龠々]/)
      @video.conversion_title = @video.title.to_kanhira.to_roman
    elsif @video.title.is_hira? || @video.title.is_kana?
      @video.conversion_title = @video.title.to_roman
    else
      @video.conversion_title = @video.title
    end
    if @video.save
      redirect_to video_path(@video)
    else
      render :new
    end
  end

  private

  def video_params
    params.require(:video).permit(:title, :introduction, :video)
  end

上から順に、まず、タイトルに漢字が含まれるか判定します。
含まれていれば、タイトルをひらがなに変換した後さらにローマ字に変換して保存します。

漢字が含まれておらず、全てひらがな、カタカナの場合はローマ字に変換し保存します。

どちらにも当てはまらない場合は、ローマ字で投稿されていると判断してそのまま保存します。

searchアクションも同様に書いていきます。

/videos_controller.rb
  def search
    word = params[:search]
    unless word.blank?
      if word.match(/[一-龠々]/)
        conversion_word = word.to_kanhira.to_roman
      elsif word.is_hira? || word.is_kana?
        conversion_word = word.to_roman
      else
        conversion_word = word
      end
    end
    @search_video = Video.search(conversion_word)
  end

フォームで検索されたワードを、wordに代入して、wordが入っていればローマ字に変換します。
createと同様に、上から順番に条件にあった変換をします。
もし検索ワードが何も無しで検索された場合は動画を全て返しています。

検索結果のviewはこんな感じ。

/videos/search.html.erb
<h2>検索結果</h2>

      <% unless @search_video.blank? %>
        <div class="row">
        <% @search_video.each do |video| %>

        ===== 省略 =====    

        <% end %>
        </div>
      <% else %>
        <p>検索結果はありません</p>
      <% end %>
  </div>

検索したワードに一致するものがなければ、その旨を表示するようにしてあります。
これで一通りできました。

完成!

実際にやってみます。

スクリーンショット 2020-09-28 13.30.22.png

ひらがなで "うみがめ" と入力

スクリーンショット 2020-09-28 13.31.12.png

"umigame" という conversion_title を持っている動画を返してくれました。
(海亀のタイトルを持っている動画がたくさんありますが、これは conversion_title を追加する前の動画です。ご愛敬。)

ローマ字でも検索してみます。

スクリーンショット 2020-09-28 14.38.18.png

スクリーンショット 2020-09-28 14.38.33.png

表示されました。

その他

テストをしながら、ブラウザバック等が入るとパラメーターの動きが変わるのか、全ての動画が読まれたりというとが発生します。
多分キャッシュとかなんだろうな...JSも勉強しないとなぁ...と思うところであります。

漢字に関しては、タイトルにもありますが100%完璧に変換してくれる訳ではないようです。
(実際、 "最強" という文字が "saikiu" と変換されていました)
ちょっとした検索を作りたい時などに利用できるかと思います。

もっといい方法があれば、ぜひお願いします。

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

active recordで特定カラムを空で保存できるように修正

解決策

下記のようにallow_blankを追加したら解決できました

validates :something, allow_blank: true

参考記事

Validate attribute only if it present (only if user fill in it)

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

【ActiveAdmin】コピペ新規作成するcloneアクションを追加する

はじめに

rails_adminの方ではclone機能をgemで追加して実装出来たのですが……
→ https://qiita.com/MATO/items/116bda1f3629ece0812c

active_adminの方ではgemではなく設定ファイルをいじって実装出来たのでメモです。
かなり汎用性あるコード書けたと思います。

ソースコード

関係ない所もありますが、見返す用に全部載せておきます。

app/admin/items.rb
ActiveAdmin.register Item do

  permit_params :kind_id, :name, :memo

  # 一覧ページで検索フィルター要らないので消しておく
  config.filters = false

  # 一覧ページでのデフォルトソート
  config.sort_order = 'updated_at_desc'

  # 間違って削除しないように処理自体を消しておく
  actions :all, :except => [:destroy]

  # カスタムアクション、clone
  member_action :clone, method: :get do
    from_item = Item.find(params[:id])
    @item     = Item.new

    # ここでコピペしておきたいカラムをもう入れてしまう
    @item[:kind_id]     = from_item.kind_id
    @item[:name]        = from_item.name

    render :new, layout: false
  end

  # 詳細ページにもCloneボタンを追加
  # indexページでエラーが出る、、、のでIF文入れておく
  action_item :only => :show do
    if params[:id].present?
      link_to "Clone Item", clone_admin_item_path(id: params[:id])
    end
  end

  index do
      # selectable_column
      # id_column

      column :kind
      column :name
      column :memo

      actions defaults: false do |item|
        item 'View', admin_item_path(item), class: 'view_link member_link'
        item 'Edit', edit_admin_item_path(item), class: 'edit_link member_link'
        item 'Clone', clone_admin_item_path(id: item.id), class: 'clone_link member_link'
      end

  end

  form do |f|
    inputs do
      input :name
      input :kind, collection: Kind.all.order(:name)
      input :memo
    end

    actions
  end


end

これで一覧ページでレコードごとにCloneリンクも追加されて、機能します。
/adminから/manageとかに名前を変更した場合でも、rails routesで表示されるパスリンク?を使えばいけます。

カスタムアクションとして

今回は「新規作成ページ」というnewと同じ処理する時にちょっとデータを入れただけ、ですね。
ただこれを元にしたら、そのレコードに対しての処理を好き放題にControllerに書くように書けたので、かなり楽しくカスタムできそうです。

参考ページ

GitHubにドンピシャの質問あったのですが、色々と方法ありすぎたり、2013年とちと古かったり、英語だし……でちょっと違いますね。
https://github.com/activeadmin/activeadmin/issues/972

俺のコードの方がキレイ?

終わり

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

【第2回】RSpecビギナーが、ビギナーなりにSystemSpecを書いてみた(沼編)

はじめに

こちらは前回の記事【第1回】RSpecビギナーが、ビギナーなりにModelSpecを書いてみたの第2回、SystemSpec(システムスペック)編です。
ModelSpec(モデルスペック)について知りたい方は前回の記事へどうぞ!

また先日、RSpecの雄である伊藤淳一 @jnchito さんのご厚意で開催された初学者向けの勉強会(RSpecビギナーズ!!)にも参加致しましたので、こちらの動画も見て頂けるとより理解が深まると思うので、もしよろしければご覧下さい。

この記事で扱うこと

  • SystemSpec(システムスペック)
    モデル、コントローラ、ビュー、全部テストできるよ!
  • システムスペックの具体的な記述例
    自身のポートフォリオを参考にして記述していきます。今回扱うのは主に個人会員と法人会員の認証のテストです。記事の投稿、編集、DM、通知のテストは次回掲載予定です。

この記事で扱わないこと

  • ModelSpec(モデルスペック)
    前回の記事をご覧下さい。
  • RSpecのセットアップ、準備
    (後述する参考書籍『EverydayRails-RSpecによるRailsテスト入門』にて詳しく記載してあるのでそちらを参考にしてください)

前提

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

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

システムスペックについて

そもそもシステムスペックって何やねん?:thinking:って方もいるかと思います。

例えば皆さんがWebサイトを利用する場面を想像してください。
アカウント登録やログインをしたり、マイページを編集したり、何か投稿したり、誰かにDMを送ったり、、、挙げればきりがないんですが、実際のブラウザ上で何かしらのアクションを起こしますよね?
システムスペックは、この実際のブラウザ上でのユーザーのアクションが正しく動作するかということをテストします。
ユーザーが実際にWebサイトを見ながら動かすものが対象なので、そりゃ大事ですよね。
そのため前回記事の終わりに、モデルスペックを書くのも大事だけどシステムスペックを書くのが大事だと言及しました。

またRSpecの記事を探していると、「フィーチャスペック」というものも出てきますが、これはシステムスペックの前身だと思ってください。
RSpec 3.7から、Rails 5.1以降のアプリケーションに対してシステムスペック(system spec)をテストスイートに追加できるようになりましたので、バージョンが上記以降のものを使っている方はシステムスペックを書いていきましょう。

ちなみにですが、システムスペックを記述していくにもいろいろ設定が必要ですので、必要なgemや設定はEverydayRailsを参考にしてください。

users_spec.rbのテスト

①FactoryBotを使用し、userデータをあらかじめ用意しておく

前回、FactoryBotを使用し事前データを用意しました。システムスペックを書く際にも使用するので、参考までに以下に載せておきます。
FactoryBotの詳細は、前回の記事を参考にしてください。

①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

②具体的なコードの記述

それではテストコードを書いていきます。
$ rails g rspec:system users を実行すると spec/system フォルダ内に users_spec.rb が作成されます。
テストコード(実際のブラウザ上の動きなど)はこちらのファイルに記述していきます。
以下、完成例です。

②spec/system/users_spec.rb
require 'rails_helper'

RSpec.describe 'User', type: :system do
  let(:user){FactoryBot.create(:user)}
  describe 'ユーザー認証のテスト' do
    describe 'ユーザー新規登録' do
      before do
        visit new_user_registration_path
      end
      context '新規登録画面に遷移' do
        it '新規登録に成功する' do
          fill_in 'user[last_name]', with: "テスト"
          fill_in 'user[first_name]', with: "太郎"
          fill_in 'user[kana_last_name]', with: "テスト"
          fill_in 'user[kana_first_name]', with: "タロウ"
          fill_in 'user[email]', with: "test@example.com"
          fill_in 'user[postal_code]', with: "1234567"
          fill_in 'user[address]', with: "東京都足立区123-12-1"
          fill_in 'user[phone_number]', with: "12345678910"
          fill_in 'user[password]', with: "testtaro"
          fill_in 'user[password_confirmation]', with: "testtaro"
          click_button '新規登録'
          expect(page).to have_content 'アカウント登録が完了しました。'
        end

        it '新規登録に失敗する' do
          fill_in 'user[last_name]', with: ""
          fill_in 'user[first_name]', with: ""
          fill_in 'user[kana_last_name]', with: ""
          fill_in 'user[kana_first_name]', with: ""
          fill_in 'user[email]', with: ""
          fill_in 'user[postal_code]', with: ""
          fill_in 'user[address]', with: ""
          fill_in 'user[phone_number]', with: ""
          fill_in 'user[password]', with: ""
          fill_in 'user[password_confirmation]', with: ""
          click_button '新規登録'
          expect(page).to have_content '個人会員 は保存されませんでした。'
        end
      end
    end

    describe 'ユーザーログイン' do
      before do
        visit new_user_session_path
      end

      context 'ログイン画面に遷移' do
        it 'ログインに成功する' do
          fill_in 'user[email]', with: user.email
          fill_in 'user[password]', with: user.password
          click_button 'ログイン'
          expect(page).to have_content 'ログインしました。'
        end

        it 'ログインに失敗する' do
          fill_in 'user[email]', with: ''
          fill_in 'user[password]', with: ''
          click_button 'ログイン'
          expect(current_path).to eq(new_user_session_path)
        end
      end
    end
  end

  describe 'ユーザーのテスト' do
    before do
      visit new_user_session_path
      fill_in 'user[email]', with: user.email
      fill_in 'user[password]', with: user.password
      click_button 'ログイン'
    end

    describe 'マイページのテスト' do
      it 'ヘッダーにマイページと表示される' do
        expect(page).to have_content('マイページ')
      end
      it 'マイページに遷移し編集リンクが表示される' do
        visit user_path(user)
        expect(page).to have_content('編集する')
      end
    end

    describe '編集のテスト' do
      context '編集画面へ遷移' do
        it '遷移ができる' do
          visit edit_user_path(user)
          expect(current_path).to eq('/users/' + user.id.to_s + '/edit')
        end
      end

      context '表示の確認と編集' do
        before do
          visit edit_user_path(user)
        end
        it '登録情報編集と表示される' do
          expect(page).to have_content('登録情報編集')
        end
        it '画像編集フォームが表示される' do
          expect(page).to have_field 'user[profile_image]'
        end
        it '名前編集フォームに自分の姓が表示される' do
          expect(page).to have_field 'user[last_name]', with: user.last_name
        end
        it '名前編集フォームに自分の名が表示される' do
          expect(page).to have_field 'user[first_name]', with: user.first_name
        end
        it '名前編集フォームに自分のカナ姓が表示される' do
          expect(page).to have_field 'user[kana_last_name]', with: user.kana_last_name
        end
        it '名前編集フォームに自分のカナ名が表示される' do
          expect(page).to have_field 'user[kana_first_name]', with: user.kana_first_name
        end
        it 'メールアドレス編集フォームに自分のメールアドレスが表示される' do
          expect(page).to have_field 'user[email]', with: user.email
        end
        it '郵便番号編集フォームに自分の郵便番号が表示される' do
          expect(page).to have_field 'user[postal_code]', with: user.postal_code
        end
        it '住所編集フォームに自分の住所が表示される' do
          expect(page).to have_field 'user[address]', with: user.address
        end
        it '電話番号編集フォームに自分の電話番号が表示される' do
          expect(page).to have_field 'user[phone_number]', with: user.phone_number
        end
        it '自己紹介文編集フォームに自分の自己紹介文が表示される' do
          expect(page).to have_field 'user[introduction]', with: user.introduction
        end
        it '編集に成功する' do
          # 名前を二郎に変更
          fill_in 'user[first_name]', with: '二郎'
          fill_in 'user[kana_first_name]', with: 'ジロウ'
          click_button '変更を保存する'
          expect(page).to have_content '会員情報の更新が完了しました。'
          expect(page).to have_content 'テスト 二郎 (テスト ジロウ)'
          expect(current_path).to eq('/users/' + user.id.to_s)
        end
        it '編集に失敗する' do
          # first_name 名前(名)を空欄で入力
          fill_in 'user[first_name]', with: ''
          click_button '変更を保存する'
          expect(page).to have_content '件のエラーが発生したため 個人会員 は保存されませんでした。'
          expect(page).to have_content '名前(名)を入力してください'
          expect(current_path).to eq('/users/' + user.id.to_s)
        end
      end
    end
  end

end

「お、なんか FactoryBot の書き方が前回と違うぞ:thinking: let(:user) って何だ??」って思った方へ。

RSpecには let という機能があります。
これを使うとインスタンス変数をlet(:変数)という形に置き換え、後のコードで変数として使用することができます。
詳しくはこちらの記事を参考にしてください。RSpecのletを使うのはどんなときか?(翻訳)
beforeブロックで囲む場合と比べるとコード量も少なくなりますし、よりRSpecらしくなるので是非使ってみましょう。
ただ参考記事にも記述してある通り、letには「遅延評価される」という特徴があるので少し注意が必要です。こちらについては後のコード例で説明します。(私が沼にハマった原因です)



では、まずはユーザー認証のテストから説明していきます。
以下のコードをご覧下さい。

②spec/system/users_spec.rb
  let(:user){FactoryBot.create(:user)}
  describe 'ユーザー認証のテスト' do
    describe 'ユーザー新規登録' do
      before do
        visit new_user_registration_path # 新規登録画面へ遷移
      end
      context '新規登録画面に遷移' do
        it '新規登録に成功する' do
          # fill_in で登録情報をテキストボックスへ入力
          fill_in 'user[last_name]', with: "テスト" 
          fill_in 'user[first_name]', with: "太郎"
          fill_in 'user[kana_last_name]', with: "テスト"
          fill_in 'user[kana_first_name]', with: "タロウ"
          fill_in 'user[email]', with: "test@example.com"
          fill_in 'user[postal_code]', with: "1234567"
          fill_in 'user[address]', with: "東京都足立区123-12-1"
          fill_in 'user[phone_number]', with: "12345678910"
          fill_in 'user[password]', with: "testtaro"
          fill_in 'user[password_confirmation]', with: "testtaro"
          click_button '新規登録' # ボタンをクリック
          expect(page).to have_content 'アカウント登録が完了しました。'
        end

まず、beforeブロック内で visit new_user_registration_path という記述があります。
この visit というものは Capybara の機能で、 visit + path で特定のページに移動できます。
Capybaraとは、ユーザーが実際にWebサイトを使用しているかのように、様々なページを遷移しその際にどこか不具合がないか調べてくれる便利な機能です(カピバラがいろんなページを走り回って調べてくれているところを想像するとほっこりしますね)。

Capybaraを使うと visit以外にも様々な便利な機能が使えます。
Railsアプリケーションを作成した際に標準で gem 'capybara' がついているかと思いますが、確認してみてください。
参考記事:使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」



さて、 ここからは実際の画面を想像しながら新規登録に成功するテストコードを記述していきます。

  • visit new_user_registration_path ⇨新規登録画面へ遷移しました。
  • fill_in 'user[last_name]', with: "テスト"⇨テストという文字列を名前(姓)テキストボックスへ入力。以下の登録情報も同じく入力する。(fill_in 'user[カラム]', with: "入力値" という形になっています。)
  • click_button '新規登録'⇨ 新規登録ボタンを押下します。
  • expect(page).to have_content 'アカウント登録が完了しました。'⇨既に使われているアドレスやパスワードを入力しない限り、アカウント登録が成功するので アカウント登録が完了しました。というメッセージが画面上に表示されるはずです。
    have_content はページ内に特定の文字列が表示されていることを確かめることができます。以降、have_◯◯という記述がたくさん出ますので、わからなければ都度上記の参考記事を確認してください。



はい、これで新規登録に成功するテストがシステムスペックで書けました!
$ bundle exec rspec spec/system/users_spec.rb をターミナルで実行してみましょう!
うまくできていればターミナルに 1 examples, 0 failures という結果が出力されます。
エラーが出た場合は、エラー文を読み、コードで間違っている箇所がないか、もしくは設定の時点で何か漏れがないかなど確認しましょう。
また、慣れてきたらexpect(page).not_to have_content 'アカウント登録が完了しました。'のように、expect(page) の後をnot_toに変更してみて"テストがちゃんと失敗すること"を再確認することも大事です。間違ったテストコードを記述してしまっている場合、わざとnot_toにしてあげているのにテストが成功してしまうことがあります。これを防ぐために敢えてnot_toなどに変更し、テストが失敗することを確認してあげましょう。

ログインやマイページ上での登録情報の編集なども、基本は同じなのでこちらを参考にしながら記述してみてください!

ちなみにclick_buttonは submitボタンやbuttonタグ の時に使います。
対してCSSによって見た目は同じように見えてもそれがリンクであるときはclick_linkを使います。
どちらでも使えるのが click_on になっています。じゃあ、全部click_onでよくね?と思ったそこのあなた。私も思いました:hugging: データを送信するためのボタンと遷移のためののリンクを明示的に分けてあげるためとか? そんな感じなのかなと自分で考えたりしたのですが、分けていることで他に何かメリットがあるのでしょうか。詳しい方教えて欲しいです。

companies_spec.rbのテスト

①FactoryBotを使用し、companyデータをあらかじめ用意しておく

前回作成したものです。

①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

②具体的なコードの記述

法人の新規登録は個人会員の新規登録とは異なり、以下の流れになっています。

  1. 法人が新規登録のためにフォームを入力し、申請するボタンをクリックする。
  2. トップ画面へリダイレクトされ、「承認済メールが届くまで今しばらくお待ちください。」というメッセージが表示される。同時に管理者へ申請通知が送られる。
  3. 承認済メールが届くまでは法人ログインが制限される。
  4. 管理者が申請通知を確認し、申請ステータスを承認済に更新する。同時に法人の登録済アドレスへ承認済メールが送信される。
  5. ログインが可能になる。


ベーコンレタスエッグつくねライスバーガーか!



取り乱しました。ややこしすぎる物事に対するくりぃむしちゅー上田さんのツッコミが出てしまいました。

基本は今までと同じなので慣れたらどうってことないのでしょうが、初学者にとっては難しく感じ苦労しました。
またこのような仕組みの参考記事が見つからなかったのが、今回の記事を書くきっかけにもなりました。(探し方が下手くそなだけかもしれませんが)
もし似たような仕組みを作っている初学者の方がいれば、こちらが参考になればと思います。

以下完成コード例です。

②spec/system/companies_spec.rb
require 'rails_helper'

RSpec.describe "Companies", type: :system do
  let!(:admin){FactoryBot.create(:admin)}
  let(:company){FactoryBot.create(:company)}
  describe 'ユーザー認証のテスト' do
    describe '法人の新規登録申請' do
      before do
        visit new_company_registration_path
      end
      it '登録申請に成功する' do
        fill_in 'company[company_name]', with: "テスト2株式会社"
        fill_in 'company[kana_company_name]', with: "テストツーカブシキガイシャ"
        fill_in 'company[email]', with: "test2company@example.com"
        fill_in 'company[postal_code]', with: "2222222"
        fill_in 'company[address]', with: "東京都千代田区222-22-2"
        fill_in 'company[phone_number]', with: "22222222222"
        fill_in 'company[password]', with: "test2company"
        fill_in 'company[password_confirmation]', with: "test2company"
        click_button '申請する'
        expect(page).to have_content '登録申請ありがとうございます。法人会員専用ページは運営にて申請が承認がされた後に閲覧可能になります。承認済メールが届くまで今しばらくお待ちください。'
      end
      it '登録申請に失敗する' do
        fill_in 'company[company_name]', with: ""
        fill_in 'company[kana_company_name]', with: ""
        fill_in 'company[email]', with: ""
        fill_in 'company[postal_code]', with: ""
        fill_in 'company[address]', with: ""
        fill_in 'company[phone_number]', with: ""
        fill_in 'company[password]', with: ""
        fill_in 'company[password_confirmation]', with: ""
        click_button '申請する'
        expect(page).to have_content "法人会員 は保存されませんでした。"
      end
    end
  end

  describe '法人がログイン可能になるまでのテスト' do
    before do
      # 法人が登録申請フォーム入力
      visit new_company_registration_path
      fill_in 'company[company_name]', with: "テスト2株式会社"
      fill_in 'company[kana_company_name]', with: "テストツーカブシキガイシャ"
      fill_in 'company[email]', with: "test2company@example.com"
      fill_in 'company[postal_code]', with: "2222222"
      fill_in 'company[address]', with: "東京都千代田区222-22-2"
      fill_in 'company[phone_number]', with: "22222222222"
      fill_in 'company[password]', with: "test2company"
      fill_in 'company[password_confirmation]', with: "test2company"
      click_button '申請する' # 通知が送信される
    end

    describe '管理者:通知の確認〜申請承認のテスト' do
      before do
        # 管理者でログイン
        visit new_admin_session_path
        fill_in 'admin[email]', with: admin.email
        fill_in 'admin[password]', with: admin.password
        click_button 'ログイン'
      end
      it 'ヘッダーに法人登録申請と表示される' do
        expect(page).to have_content('法人登録申請')
      end
      it '法人登録申請一覧に申請してきた法人名が表示される' do
        visit admin_notifications_path
        expect(page).to have_content("テスト2株式会社 様からの法人登録申請があります")
      end
      it 'リンクから企業詳細ページへ遷移できる' do
        visit admin_notifications_path
        notification = Notification.find_by({receiver_id: admin.id, receiver_class: "admin", sender_id: Company.last.id, sender_class: "company"})
        # find("#request_message").click # ページに同一の文言のリンクがある場合(今回の場合「法人登録申請」)、idを指定してあげる
        find("#request_message").click
        expect(current_path).to eq('/admin/companies/' + notification.sender_id.to_s)
      end
      it '編集画面へ遷移する' do
        visit admin_company_path(Company.last.id)
        click_link '編集する'
        expect(current_path).to eq('/admin/companies/' + Company.last.id.to_s + '/edit')
      end
      it '申請ステータスを承認済にする' do
        visit edit_admin_company_path(Company.last.id)
        choose "company_approved_true" # 申請ステータスを承認済にチェック(company_approved_trueはラジオボタン要素のid)
        click_button '変更を保存する'
        expect(page).to have_content '企業情報の更新が完了しました。'
        expect(current_path).to eq('/admin/companies/' + Company.last.id.to_s)
      end
    end

    describe '法人:ログインのテスト' do
      context '承認前の法人ログイン' do
        it 'ログインに失敗し、メール受信後に再度ログインするようメッセージが出る' do
          visit new_company_session_path
          fill_in 'company[email]', with: "test2company@example.com"
          fill_in 'company[password]', with: "test2company"
          click_button 'ログイン'
          expect(page).to have_content '登録申請が未承認です。申し訳ございませんが、承認済メールが届くまで今しばらくお待ちください。'
        end
      end

      context '承認後の法人ログイン' do
        before do
          login_as(admin) # 管理者ログイン
          visit edit_admin_company_path(Company.last.id)
          choose "company_approved_true" # 申請ステータスを承認済にチェック(company_approved_trueはラジオボタン要素のid)
          click_button '変更を保存する'
          click_on 'ログアウト'
          visit new_company_session_path
        end
        it 'ログインに成功する' do
          fill_in 'company[email]', with: "test2company@example.com"
          fill_in 'company[password]', with: "test2company"
          click_button 'ログイン'
          expect(page).to have_content 'ログインしました。'
        end
        it 'ログインに失敗する' do
          fill_in 'company[email]', with: ""
          fill_in 'company[password]', with: ""
          click_button 'ログイン'
          expect(current_path).to eq(new_company_session_path)
        end
      end
    end
  end

  describe '法人会員のテスト' do
    before do
      visit new_company_session_path
      fill_in 'company[email]', with: company.email
      fill_in 'company[password]', with: company.password
      click_button 'ログイン'
    end

    describe 'マイページのテスト' do
      it 'ヘッダーにマイページと表示される' do
        expect(page).to have_content('マイページ')
      end
      it 'マイページに遷移し編集リンクが表示される' do
        visit corporate_company_path(company)
        expect(page).to have_content('編集する')
      end
    end

    describe '編集のテスト' do
      before do
        visit edit_corporate_company_path(company)
      end
      context '編集画面へ遷移の確認' do
        it '遷移ができる' do
          expect(current_path).to eq('/corporate/companies/' + company.id.to_s + '/edit')
        end
      end
      context '表示及び編集の確認' do
        it '登録情報編集と表示される' do
          expect(page).to have_content('登録情報編集')
        end
        it 'プロフィール画像編集フォームが表示される' do
          expect(page).to have_field 'company[profile_image]'
        end
        it 'ヘッダー画像編集フォームが表示される' do
          expect(page).to have_field 'company[background_image]'
        end
        it '企業名編集フォームに企業名が表示される' do
          expect(page).to have_field 'company[company_name]', with: company.company_name
        end
        it 'フリガナ編集フォームに自分の企業カナ名が表示される' do
          expect(page).to have_field 'company[kana_company_name]', with: company.kana_company_name
        end
        it 'メールアドレス編集フォームに自分のメールアドレスが表示される' do
          expect(page).to have_field 'company[email]', with: company.email
        end
        it '郵便番号編集フォームに自分の郵便番号が表示される' do
          expect(page).to have_field 'company[postal_code]', with: company.postal_code
        end
        it '住所編集フォームに自分の住所が表示される' do
          expect(page).to have_field 'company[address]', with: company.address
        end
        it '電話番号編集フォームに自分の電話番号が表示される' do
          expect(page).to have_field 'company[phone_number]', with: company.phone_number
        end
        it '自己紹介文編集フォームに自分の自己紹介文が表示される' do
          expect(page).to have_field 'company[introduction]', with: company.introduction
        end
        it '編集に成功する' do
          fill_in 'company[introduction]', with: "テスト株式会社のマイページへようこそ!"
          click_button '変更を保存する'
          expect(page).to have_content '企業情報の更新が完了しました。'
          expect(current_path).to eq('/corporate/companies/' + company.id.to_s)
        end
        it '編集に失敗する' do
          fill_in 'company[company_name]', with: ""
          click_button '変更を保存する'
          expect(page).to have_content '件のエラーが発生したため 法人会員 は保存されませんでした。'
        end
      end
    end
  end
end



まず、法人がログインできるようになるには管理者による申請の承認が必要なため、先に管理者を FactoryBot で作成します。$ bin/rails g factory_bot:model admin を実行し、必要なサンプルデータを入れましょう。以下、サンプルデータを入れたファイルです。

spec/factories/admins.rb
FactoryBot.define do
  factory :admin do
    email { "testadmin@example.com" }
    password { "testadmin"}
  end
end

法人の新規登録申請と、最後の登録情報編集のテストに関しては users_spec.rb のものとほぼ同じなので説明は省略します。



その間の法人がログイン可能になるまでのテストを説明していきます。
以下のコードをご覧下さい。

②spec/system/companies_spec.rb
  describe '法人がログイン可能になるまでのテスト' do
    before do
      # 1.法人が登録申請フォーム入力
      visit new_company_registration_path
      fill_in 'company[company_name]', with: "テスト2株式会社"
      fill_in 'company[kana_company_name]', with: "テストツーカブシキガイシャ"
      fill_in 'company[email]', with: "test2company@example.com"
      fill_in 'company[postal_code]', with: "2222222"
      fill_in 'company[address]', with: "東京都千代田区222-22-2"
      fill_in 'company[phone_number]', with: "22222222222"
      fill_in 'company[password]', with: "test2company"
      fill_in 'company[password_confirmation]', with: "test2company"
      click_button '申請する' # 通知が送信される
    end

    describe '管理者:通知の確認〜申請承認のテスト' do
      before do
        # 2.管理者でログイン
        visit new_admin_session_path
        fill_in 'admin[email]', with: admin.email
        fill_in 'admin[password]', with: admin.password
        click_button 'ログイン'
      end
      # 3.法人登録申請という文言があることから、管理者ログインができていることを確認
      it 'ヘッダーに法人登録申請と表示される' do
        expect(page).to have_content('法人登録申請')
      end
      # 4.法人登録申請のリンクをクリックし、申請企業名を確認
      it '法人登録申請一覧に申請してきた法人名が表示される' do
        visit admin_notifications_path
        expect(page).to have_content("テスト2株式会社 様からの法人登録申請があります")
      end
      # 5.申請一覧から法人登録申請というリンクをクリックする
      it 'リンクから企業詳細ページへ遷移できる' do
        visit admin_notifications_path
        notification = Notification.find_by({receiver_id: admin.id, receiver_class: "admin", sender_id: Company.last.id, sender_class: "company"})
        # find("#request_message").click # ページに同一の文言のリンクがある場合(今回の場合「法人登録申請」)、idを指定してあげる
        find("#request_message").click
        expect(current_path).to eq('/admin/companies/' + notification.sender_id.to_s)
      end
      # 6.登録情報編集ページへ
      it '編集画面へ遷移する' do
        visit admin_company_path(Company.last.id)
        click_link '編集する'
        expect(current_path).to eq('/admin/companies/' + Company.last.id.to_s + '/edit')
      end
      # 7.ラジオボタンに注意
      it '申請ステータスを承認済にする' do
        visit edit_admin_company_path(Company.last.id)
        choose "company_approved_true" # 申請ステータスを承認済にチェック(company_approved_trueはラジオボタン要素のid)
        click_button '変更を保存する'
        expect(page).to have_content '企業情報の更新が完了しました。'
        expect(current_path).to eq('/admin/companies/' + Company.last.id.to_s)
      end
    end

    describe '法人:ログインのテスト' do
      context '承認前の法人ログイン' do
        it 'ログインに失敗し、メール受信後に再度ログインするようメッセージが出る' do
          visit new_company_session_path
          fill_in 'company[email]', with: "test2company@example.com"
          fill_in 'company[password]', with: "test2company"
          click_button 'ログイン'
          expect(page).to have_content '登録申請が未承認です。申し訳ございませんが、承認済メールが届くまで今しばらくお待ちください。'
        end
      end

      context '承認後の法人ログイン' do
        before do
          login_as(admin) # 管理者ログイン
          visit edit_admin_company_path(Company.last.id)
          choose "company_approved_true" # 申請ステータスを承認済にチェック(company_approved_trueはラジオボタン要素のid)
          click_button '変更を保存する'
          click_on 'ログアウト'
          visit new_company_session_path
        end
        it 'ログインに成功する' do
          fill_in 'company[email]', with: "test2company@example.com"
          fill_in 'company[password]', with: "test2company"
          click_button 'ログイン'
          expect(page).to have_content 'ログインしました。'
        end
        it 'ログインに失敗する' do
          fill_in 'company[email]', with: ""
          fill_in 'company[password]', with: ""
          click_button 'ログイン'
          expect(current_path).to eq(new_company_session_path)
        end
      end
    end
  end



私「よし、ファイルも作成されたし let(:admin){FactoryBot.create(:admin)} と書いてっと...これでadminという変数が使えるようになったぜ!」

一見問題ないように見える上記の記述、これが沼への入り口だったのです。

  1. 法人が登録申請フォームを入力
    beforeブロックでは法人が登録申請フォームを入力し、申請するボタンを押したところまでを記述します。
    この時点で管理者へ申請通知が送信されている状態です。

  2. つまづきポイント:hugging: 管理者でログイン「letの遅延評価」
    私「よし、申請承認するためにまずは管理者でログインっと...あれ?なんかめちゃくちゃテスト失敗するんですが、なぜ...??」
    私はここでテストが失敗する理由をしばらく見つけることができず、負のスパイラルへ陥りました。
    よく調べてみると先ほどの let(:admin){FactoryBot.create(:admin)} という記述、注目すべきはlet(:admin)の部分。一見何も問題がないように見えますが、先にちょこっと話していた通り、letには「遅延評価される」という特徴があるのです。
    遅延評価って何やねん?:thinking:という方はこちらの記事をどうぞ。
    RSpecのletでcreateした時に気をつけること 〜遅延評価〜
    RSpecのletを使うのはどんなときか?(翻訳)
    こちらの記事によると、ただのletでcreateした時は、代入した変数が参照されたタイミングで評価されるんだそう=遅延評価。
    つまり、fill_in 'admin[email]', with: admin.emailのように admin という変数が使用されたタイミングでなければ、adminのデータ内容というのは無いものとみなされます。なるほど、でもadminって書いてるんだよな〜と思いながらlet!にしてみると無事テストが通りました。
    うむ、、通ったけどなぜだろう... describeがいくつか重なっていることが原因かなとも思いましたが、結局わかりませんでした。。詳しい方、教えてください。。

  3. 法人登録申請という文言があることから、管理者ログインができていることを確認
    管理者ログイン後のヘッダーには法人登録申請というリンクがあるため、その表示があることを確かめ、ログインができたことを確認します。

  4. 法人登録申請のリンクをクリックし、申請企業名を確認
    ヘッダーのリンクをクリックすると、企業名が表示されるので 1で申請した企業であるかを確認

  5. つまづきポイント:hugging:「申請一覧から法人登録申請というリンクをクリックする」
    ここでもつまづきました。注目すべきはfind("#request_message").clickの部分。今やりたいことは「法人登録申請というリンクをクリックし、企業詳細ページへ遷移する」ということです。普通であればclick_linkかclick_onで遷移できるはずです。しかし上手くいかずエラーが出ました。この時のエラー文がこちら。

Capybara::Ambiguous:
Ambiguous match, found 2 elements matching visible link "法人登録申請"

これを要約すると
カピバラ「あの、"法人登録申請"というリンクが2つあるんですが、僕はどっちに行けばいいですか??」
という意味になります。私はここで気づきました。

法人登録申請
2つあるやん...カピバラごめん...
ということで改善法をググった結果、click_linkではなく、ビューの対象部分に固有のid名(request_message)を指定し、テストコードはfind("#request_message").clickという記述で解決しました。idを指定して、対象の方をクリックしてねということですね。

<strong><%= notification.sender_name %></strong> 様からの<%= link_to '法人登録申請', admin_company_path(notification.sender_id), id: "request_message" %>があります。登録内容を確認してください。

6.登録情報編集ページへ
ここは簡単ですね。特に説明はしません。

7.つまづきポイント:hugging:「ラジオボタンに注意」
Capybara の choose という機能を使い、申請ステータスを承認済にチェックします。
choose "company_approved_true"という形で記述します。 "company_approved_true" ってどこからきたかというと、googleの検証ツールを使うと、inputタグの中にid名が割り振られていることが分かります。ラジオボタンはそのidを指定して選択しているという形になっています。ただ、他の記事を見た際にlabelタグ内の文字列をターゲットに選択している記事もあります。今回の場合であれば、"承認済"という文字列です。ブラウザ上で見る際は、普通であれば文字列を見て選択すると思います。その為、「検証ツールでidを指定して選択」という方法はできるだけしない方が良いのかもしれません。私は文字列を指定するとテストが通らなかった為、このような形にしています。こちらも詳しい方がいらっしゃいましたら、教えていただきたいです。エラーは解決できても、"それがなぜなのか"という部分が解決できていないのでこの記事を見ていらっしゃる方には申し訳ないです。。逆にわかったら教えてください:bow_tone1:

法人登録申請

はい!ということで管理者による通知の確認〜申請承認のテストはこれで終わりです!

最後に承認前と承認後でログイン制限がされているか否かを確認するテストを書きます。
ここまで見てくださった方は、恐らく分かると思いますのでコードを参考にしながら記述してみてください!

ポイントとして、承認後の法人ログインのテストにあるbeforeブロック内のlogin_as(admin)という記述だけチラッと説明します!
やっていることは単純に管理者でログインしているだけなのですが、"login_as"という機能は前回の記事でお話しした "deviseのヘルパーメソッドをRSpec内で使用可能にする設定" がされていないと、使うことができませんのでここだけ注意です!
何だっけ?と思った方は、前回の記事【第1回】RSpecビギナーが、ビギナーなりにModelSpecを書いてみたをご覧下さい。

終わりに

今回は主にシステムスペックによる個人会員と法人会員の認証のテストについてまとめました。
記事の投稿、編集、DM、通知のテストも書こうと思ってたのですが、想像よりも長くなってしまったので一旦ここで分けます。
こちらの機能のシステムスペックに関しては次回掲載致しますので、もしよろしければ見て頂けると嬉しいです。

最後までご覧頂きありがとうございました!

参考記事

使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」
RSpecのletでcreateした時に気をつけること 〜遅延評価〜
RSpecのletを使うのはどんなときか?(翻訳)

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

【Rails6】GraphQLを使用したAPI開発(Query編)

はじめに

rails6でGraphQLを用いた開発を行ったので、導入方法や使い方をまとめてみました。
今回はQuery/アソシエーション/N+1問題を取り上げて、ご紹介します。

開発環境

ruby2.7.1
rails6.0.3
GraphQL

1. GraphQLって何?

GraphQLとはAPIリクエストのためクエリ言語で下記の特徴があります。

  • エンドポイントは/graphql1つのみ
  • Query: データの取得(Get)
  • Mutation: データの作成、更新、削除(Create, Update, Delete)

RESTとの大きな違い一つ目のエンドポイントが一つということです。
RESTの場合、/sign_up, /users, /users/1など複数のエンドポイントが存在しますが、
GraphQLの場合は、エンドポイントは/graphqlのみです。

RESTでは複数のリソースで必要な場合、複数のAPIリクエストが必要ですが、
GraphQLはエンドポイントが一つなので、必要なデータを一回で取得できコードがシンプルになります。

2. 今回使用するテーブル

親のUserテーブルと子のPostテーブルの二つで実装します。
Userが複数のPostを投稿できる1対多の関係とします。

ターミナル
$ rails g model User name:string email:string
$ rails g model Post title:string description:string user:references
$ rails db:migrate

Userテーブル

カラム
name string
email string
user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end

Postテーブル

カラム
title string
description string
post.rb
class Post < ApplicationRecord
  belongs_to :user
end

3. railsにGraphQLを導入する

gemをインストールします。

Gemgile
gem 'graphql'    #追加

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'graphiql-rails'   #開発環境に追加
end
config/application.rb
require "sprockets/railtie"    #コメントアウトを外す
ターミナル
$ bundle install
$ rails generate graphql:install   #GraphQLに関するファイルが作成されます

routes.rbに下記を追加
エンドポイントが開発環境では/graphiql, 本番環境では/graphqlとなります。

routes.rb
Rails.application.routes.draw do
  if Rails.env.development?
    # add the url of your end-point to graphql_path.
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" 
  end
  post '/graphql', to: 'graphql#execute'  #ここはrails generate graphql:installで自動生成される
end

詳細は下記にまとめましたので参考にしてください。
Rails6のAPIモードでGraphQLを使う方法(エラー対策も含む)

4. User一覧を取得するQuery

GraphQLではType(modelごとに定義する)を持っており、そのTypeに従い、Queryを実行してデータを取得します。

UserのObjectTypeを作成

下記のコマンドでUserのTypeを作成します。
!をつけるとnull:falseが追加されます。

ターミナル
$ rails g graphql:object User id:ID! name:String! email:String!

下記のファイルが生成されます。

user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false  # `!`をつけると`null:false`が追加されます。
    field :name, String, null: false
    field :email, String, null: false
  end
end

UserのQueryを作成

先ほど作成したuser_typeを元にqueryを作成します。

query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false # userを配列で定義する
    def users
      User.all # user一覧を取得
    end
  end
end

Userのデータをコンソールで作成します。

$ rails c
$ > User.create(name: "user1", email: "user-1@test.com")
$ > User.create(name: "user2", email: "user-2@test.com")

Queryを実行する

準備は整いましたので、サーバーを立ち上げてGraphiqlで確認します。(http://localhost:3000/graphiql)

$ rails s

下記のqueryを実行します。

query{
  users{
    id
    name
    email
  }
}

するとjson形式のレスポンスが返ってきます。
usersのTypeを配列にしているので配列となっています。

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "user1",
        "email": "user-1@test.com"
      },
      {
        "id": "2",
        "name": "user2",
        "email": "user-2@test.com"
      }
    ]
  }
}

http://localhost:3000/graphiql に接続して、実行した結果です。
スクリーンショット 2020-09-27 20.32.34.png

必要なデータだけリクエストし受け取る場合

すべてカラムのデータが必要でない場合はqueryを変更します。
例えばuserのidだけ取得することもできます。

query{
  users{
    id
  }
}

レスポンス

{
  "data": {
    "users": [
      {
        "id": "1",
      },
      {
        "id": "2",
      }
    ]
  }
}

5. アソシエーション

次にUserに紐づくPostを取得してみます。
Userを同じようにObjectTypeとQueryを作成します。

PostのObjectTypeを生成

ターミナル
$ rails g graphql:object Post id:ID! title:String! description:String!

下記のファイルが生成されます。
Userのデータを取得するためにfield :user, Types::UserType, null: falseを追加します。

post_type.rb
module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :title, String, null: false
    field :description, String, null: false
    field :user, Types::UserType, null: false # この一文を追加。belongs_to :userのようなもの
  end
end

UserTypeにはfield :posts, [Types::PostType], null: falseを追加します。
こちらは複数データが紐づくので配列で定義します。

user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: false
    field :posts, [Types::PostType], null: false # この一文を追加。has_many :postsのようなもの
  end
end
ターミナル
$ rails c
$ > Post.create(title: "title", description: "description", user_id: 1)

PostのQueryを生成する

query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false
    def users
      User.all
    end

    # 下記を追加
    field :posts, [Types::PostType], null: false
    def posts
      Post.all
    end
  end
end

Queryを実行する

実行するqueryです。
usersにpostをネストさせてリクエストします。

query{
  users{
    id
    name
    email
    post{
      id
      title
      description
    }
  }
}

実行するとネストしたPostのデータが返ってきます。
RESTの場合でいうuser.postsのデータを取得できます。

スクリーンショット 2020-09-27 21.00.34.png

post.userのデータが必要な場合は、下記のようなqueryで取得できます。

query{
  posts{
    id
    title
    description
    user{
      id
    }
  }
}

スクリーンショット 2020-09-28 9.54.16.png

6. N+1を検知するBulletを導入する

GraphQLのクエリは木構造になっているので、アソシエーションがあるとN+1問題が発生しやすいです。
そこで、N+1を検知するgemであるBulletを導入することをおすすめします。

Bulletをインストール

Gemfile
group :development do
  gem 'bullet'
end
ターミナル
$ bundle install

config/environments/development.rbに下記の設定を追加

config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
end

N+1を確認してみる

Bulletの導入が完了したら、N+1が発生していないか確認してみましょう

query{
  users{
    id
    name
    email
    posts{
      id
      title
      description
    }
  }
}

先ほどと同様のqueryを実行すると下記のようなログが出ます。

ターミナル
Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil, "graphql"=>{"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil}}
  User Load (0.2ms)  SELECT "users".* FROM "users"
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
Completed 200 OK in 36ms (Views: 0.3ms | ActiveRecord: 1.7ms | Allocations: 18427)


POST /graphql
USE eager loading detected
  User => [:posts]
  Add to your query: .includes([:posts])
Call stack

Add to your query: .includes([:posts])と言われているのでN+1が発生しています。
SQLも三回発行されています。

ターミナル
User Load (0.2ms)  SELECT "users".* FROM "users"
Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]

N+1を解消する方法

N+1を解消するには通常通り、includesすれば大丈夫です。
dataloaderというものでも解消できますが、今回はincludesを使用します。
User一覧を取得している部分を下記のように変更します。

query_type.rb
def users
  # User.all  # 変更前
  User.includes(:posts).all # 変更後
end

では、N+1が解消されたかログで確認してみましょう。
警告がなくなっています。

ターミナル
Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil, "graphql"=>{"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil}}
  User Load (0.7ms)  SELECT "users".* FROM "users"
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?)  [["user_id", 1], ["user_id", 2]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
Completed 200 OK in 57ms (Views: 0.2ms | ActiveRecord: 1.9ms | Allocations: 15965)

SQL文も2つに減ったので無事にN+1が解消されました。

ターミナル
User Load (0.7ms)  SELECT "users".* FROM "users"
Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?)  [["user_id", 1], ["user_id", 2]]

7. resolverの切り出し

通常のQuery

普通にコードを書いていくと、モデルに関係なく、fieldsとメソッドがquery_typeに追加されるのでquery_typeがどんどん肥大化します。

query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false
    def users
      User.includes(:posts).all
    end

    field :posts, [Types::PostType], null: false
    def posts
      Post.all
    end
  end
end

Resolverを使ったQuery

GitHubのissueでは、Resolverを使用することで、query_type.rbの肥大化を回避するベストプラクティスが紹介されています。
https://github.com/rmosolgo/graphql-ruby/issues/1825#issuecomment-441306410

query_typeにはfieldのみを定義します。

query_type.rb
module Types
  class QueryType < BaseObject
    field :users, resolver: Resolvers::QueryTypes::UsersResolver
    field :posts, resolver: Resolvers::QueryTypes::PostsResolver
  end
end

そして、メソッドの部分はObjectTypeごとにResolverに切り出します。(新たにresolversディレクトリを作成しました。)
GraphQL::Schema::Resolverの記載を忘れるとエラーが出るので忘れないように注意してください。

resolvers/query_types/users_resolver.rb
module Resolvers::QueryTypes
  class UsersResolver < GraphQL::Schema::Resolver
    type [Types::UserType], null: false
    def resolve
      User.includes(:posts).all
    end
  end
end
resolvers/query_types/posts_resolver.rb
module Resolvers::QueryTypes
  class PostsResolver < GraphQL::Schema::Resolver
    type [Types::PostType], null: false
    def resolve
      Post.all
    end
  end
end

終わりに

GraphQLついて、自分の復習もかねてまとめていたら思いの外、長文となってしまいました。
rspecやmutationについても書きたかったですが、次回にしたいと思います。

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

プログラムにおける処理の順番 

学習期間が現在11週目に突入。今まで学んだ事の振り返りと、よく間違えている事について紹介します。

今回は「処理の順番」です。基本中の基本で初歩的な内容ですが、記述の順番を間違えておりエラーが発生していたというケースも多々あるので、学習し始めたばかりの方の参考になれば幸いです。
また現在学習している言語は「Ruby」ですので、内容も「Ruby」のものとなります。

使用しているrubyのバージョンは以下の通り。

ruby 2.6.5

処理の順番について

まずソース内でのプログラム処理の順番は、基本的に「上から下へ1行ずつ順番に」です。
これは複雑な記述やRuby on Railsを使ってアプリを作成した際でも変わりません。(※メソッドの呼び出しや条件分岐、繰り返し処理を行う際は下から上へ戻ったり行が飛んだりしますが、基本的な処理の順番を意識しておくと理解しやすいと思います。)
簡単なプログラムで表現すると、

puts "1行目"
puts "2行目"
puts "3行目"

と記述した場合、コンソールで実行すると

1行目
2行目
3行目

となります。この記述でputs "3行目"というプログラムの出力結果が1行目に来ることはありません。あったら大変です。
これに条件分岐(ifやwhileなど)や繰り返し処理(timesやeachなど)が重なってくると、どうしても処理の順番を疎かにしがちです。(私だけかも知れませんが・・・)

例えば、有名な「FizzBuzz問題」を例に出すと、

FizzBuzz問題
 1~100の数字を出力させるプログラムを作成します。
3の倍数の時は「Fizz」と表示し5の倍数の時は「Buzz」と
表示させるプログラムを作りなさい。
ただし15の倍数の時は「FizzBuzz」と表示させなさい。

この答えの一例は、

#正しいコード
def fizz_buzz
  num = 0
  while num <= 99
    num += 1
    if num % 15 == 0 
    puts "FizzBuzz"
    elsif num % 3 == 0
    puts "Fizz"
    elsif num % 5 == 0
    puts "Buzz"
    else
    puts num
    end
  end
end

puts fizz_buzz

出力結果は以下の通り

スクリーンショット 2020-09-28 10.56.51.png

このプログラムの条件式の順番を以下のように変えてみましょう。

#間違ったコード

def fizz_buzz
  num = 0
  while num <= 99
    num += 1
    if num % 3 == 0  # num % 15 == 0の条件式から変更
    puts "Fizz"
    elsif num % 5 == 0
    puts "Buzz"
    elsif num % 15 == 0 #num % 3 == 0の条件式から変更
    puts "FizzBuzz"
    else
    puts num
    end
  end
end

puts fizz_buzz

上記のように記述した場合の出力結果がこちら。

スクリーンショット 2020-09-28 11.07.44.png

15の倍数の時に"FizzBuzz"と表示されず"Fizz"と表示されます。
こうなってしまう理由は「プログラムの処理の順番を理解していないから」。

上記の間違ったコードの処理の順番を考えると、
例えばnum == 3の場合、

#間違ったコード
    if num % 3 == 0  # この式が適用される
    puts "Fizz"

=> "Fizz"

num == 5の場合

#間違ったコード
    if num % 3 == 0  # num == 5は3で割り切れないので、次の処理をする
    puts "Fizz"
    elsif num % 5 == 0 # num == 5は5で割り切れるので、この処理を実行
    puts "Buzz"

=> "Buzz"

問題のnum == 15の場合

#間違ったコード
    if num % 3 == 0  # num == 15は3で割り切れるから、この処理を実行!
    puts "Fizz"
    elsif num % 5 == 0 # num == 15は処理されたから仕事なくなった。
    puts "Buzz"
    elsif num % 15 == 0 # num == 15は処理されたから仕事なくなった。
    puts "FizzBuzz"
    end

=> "Fizz"

こんな感じで処理がされてしまいます。なので、プログラムを書く際の順番を気をつけましょう。

if num % 15 == 0 # まずnumが15で割り切れるかどうかを判断する。
    puts "FizzBuzz" # 条件式に当てはまれば"FizzBuzz"と表示当てはまらなければ次へ

  elsif num % 3 == 0 # 次に15で割り切れないけど3で割り切れるかを判断
    puts "Fizz"      # 条件式に当てはまれば"Fizz"と表示。
                     # これも当てはまらなければ次へ

  elsif num % 5 == 0 # 3でも15でも割り切れないけど、5で割り切れるか判断。
    puts "Buzz"      # 条件式に当てはまれば"Buzz"と表示。
                     # これも当てはまらなければ次へ
  else

   puts num         # これまでの条件に全て当てはまらない場合、
end                 # そのままnumを表示

以上。
基本的な事ですがこれまでの経験から、こういった基本がしっかり身についておらず困ったという場面がありましたので、もし同じような場面に直面したら参考にしていただければ幸いです。

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

最後に任意の文字があるかのメソッドを作る

【概要】

1.結論

2.どのようにコーディングするか

3.開発環境

1.結論

downcaseメソッド、lengthメソッドと、sliceメソッド3種類を使う!

2.どのようにコーディングするか

def str_discrimination(str_a, str_b)
  a_down_str = str_a.downcase #---❶
  b_down_str = str_b.downcase
  a_len_str = a_down_str.length #---❷
  b_len_str = b_down_str.length
  if b_down_str.slice(-(a_len_str)..- 1) == a_down_str || a_down_str.slice(-(b_len_str)..- 1) == b_len_str #---❸
    puts "True"
  else
    puts "False"
  end
end

❶:大文字小文字で区別しないためです。任意の2種類の文字列(str_a,str_b)をdowncaseメソッドで全てを小文字にしました。

変数(文字列).downcase

❷:❶で小文字に変換した変数をlengthメソッドで文字数を返しています。これは❸で使うためにコーディングしています。

❸:ここで最後に任意の文字があるかを判別しています。str_aやstr_bのどちらに探されたい文字列、探したい文字が来てもいいようにOR条件にしています。またsliceメソッドを使用し、❷で使用したlengthメソッドで代入された変数を入れることで最後から任意の文字を探しています。(-(a_len_str)..- 1)は、最後から(-1)から-(a_len_str)の文字数までを示しています。

参考にしたURL:
はじめてのRuby!文字列を大文字⇔小文字に変換する方法まとめ
length、size、count メソッドの違いまとめ【Ruby】

3.開発環境

Ruby 2.6.5
Rails 6.0.3.3
Visual Studio Code

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

CodeWarでの勉強(ruby)④ case~when

この記事について

最近始めたCodewarを通じて学べたことを少しずつアウトプット

問題

You probably know the "like" system from Facebook and other pages. People can "like" blog posts, pictures or other items. We want to create the text that should be displayed next to such an item.
Implement a function likes :: [String] -> String, which must take in input array, containing the names of people who like an item. It must return the display text as shown in the examples:

引数で配列を受け取るlikesメソッドを実装して、配列に含まれる情報から誰がイイねをしたか表示させるようにさせる。

likes [] -- must be "no one likes this"
likes ["Peter"] -- must be "Peter likes this"
likes ["Jacob", "Alex"] -- must be "Jacob and Alex like this"
likes ["Max", "John", "Mark"] -- must be "Max, John and Mark like this"
likes ["Alex", "Jacob", "Mark", "Max"] -- must be "Alex, Jacob and 2 others like this"

僕の回答

def likes(names)
  return "no one likes this" if names.empty?

  if names.count == 1
    "#{names[0]} likes this"
  elsif names.count == 2
    names.each { |n| }.join(" and ") << " like this"
  elsif names.count == 3
    "#{names[0]}, #{names[1]} and #{names[2]} like this"
  else
   others_count = names.count -2
    "#{names[0]}, #{names[1]} and #{others_count} others like this"   
  end
end

配列の長さによって返す情報を変えていくシンプルのやり方だと思う。
強調しておきたいのが、return "no one likes this" if names.empty?というように人生で初めて1行完結のif文を書けたことだ!!!!!

ベストプラクティス

めっちゃ見やすい。。。
複数の条件でやる場合はcase~whenの方が読みやすくてイイね。

def likes(names)
  case names.size
  when 0 
    "no one likes this"
  when 1 
    "#{names[0]} likes this"
  when 2
    "#{names[0]} and #{names[1]} like this"
  when 3
    "#{names[0]}, #{names[1]} and #{names[2]} like this"
  else
    "#{names[0]}, #{names[1]} and #{names.size - 2} others like this"
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]データベースに保存された情報をviewに表示する方法

投稿の内容

今回はデータベースに保存された情報をviewに表示する方法について投稿します。
なお、既にデータベースに情報は保存されている、として投稿します。

実装の流れは以下の通り。

① コントローラーアクションの記述(showアクション)
② 表示したい情報が文字列の場合
③ 表示したい情報が複数枚画像の場合

環境

Rails 5.2.4.3
ruby 2.5.1
mysql 14.14
viewはhamlで実装

① コントローラーアクションの記述(showアクション)

今回は商品(item)詳細ページに情報を表示したいので、itemsコントローラーのshowアクションを使います。

items.controller.rb
class ItemsController < ApplicationController
#省略

  def show
    @item = Item.find(params[:id])
  end

#省略

解説) まずItemモデルのpathのidをfindメソッドで検索し、該当するidに属した商品(item)のインスタンスを作成します。← 分かりにくければすいません...。

ここで作成したインスタンスをもとに、データベースから情報を引き出します。

② 表示したい情報がテキストの場合

image.png
データベースに保存されている情報が上記画像のような場合、= @item.カラム名と指定してあげるとviewに表示できます。

例えば、nameカラムの情報を表示したい場合は以下のようにすればOKです。

show.html.haml
%h2.item-show-page__item-name
  = @item.name

③ 表示したい情報が複数枚画像の場合

今回はitemsテーブルとアソシエーションを組んでいるitem_imagesテーブルに保存されている複数枚画像を表示したいとします。

show.html.haml
- @item.item_images.each do |image|
  = image_tag image.image.url

解説) まず、itemsテーブル(@item)とアソシエーションを組んでいるitem_imagesテーブルをブロック変数imageに変換します。次に、image_tagを使い、先ほど変換したブロック変数image(item_imagesテーブル)のimageを呼び出し、.urlを付けます。

この.urlはデータベースから情報を選択し、viewに表示させる場合必須となります。.urlがなければviewにはデータベースに保存されている画像URLが表示されてしまします。

あとはeach文で繰り返し処理ですね。

最後に

今回はデータベースに保存された情報をviewに表示する方法について投稿しました。
Railsでアプリケーションを開発する際に、必ず用いる方法と言っても過言ではないと思いますので、是非参考にしていただければと思います。

最後まで目を通していただきありがとうございました!

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

Rails6 OmniAuth activestorage ユーザー画像を取得する

永遠の初心者による自分のためのメモ。
Rails学習開始3ヶ月。
とりあえずこれで動きました程度に考えていただければ幸いです。
こうした方がええやんと言うご意見がある方は優しいコメント頂けると嬉しいです。

この記事の目的

active storageにユーザーのプロフィール画像を保存する。
ググってみてもcarrierwaveばかりでactive storageの記事をあまり見かけなかったので、同じ境遇の人がいればと思い投稿ました。

環境

Ruby 2.7.1p83
Rails 6.0.3.3

前提条件


deviseによるログイン機能実装済み。


OmniAuthによるTwitter、google、facebookなどのログイン認証機能実装済み。


私は、以下の記事を参考にさせて頂きました


・手続き関連
https://qiita.com/kazuooooo/items/47e7d426cbb33355590e
・OmniAuth導入
https://qiita.com/LuckHackMahiro/items/9dfca6e67777a2161240

参考

・active storageに画像URLを保存する方法
https://qiita.com/gomasio1010/items/09c6ee58ed4c95f109ff

とても助かりました!ありがとうございます。

active storageに画像urlを保存する

機能実装できている前提で、

app/models/user.rb

require "open-uri"  #ここ
class User < ApplicationRecord

  #省略

  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.provider = auth.provider
      user.uid = auth.uid
      user.name = auth.info.name
      user.password = Devise.friendly_token[0, 20]
      user.email = auth.info.email    
      user.email = User.dummy_email(auth) if user.provider == "twitter" 
      avatar = open("#{auth.info.image}") #ここ
      user.image.attach(io: avatar, filename: "user_avatar.jpg") #ここ
    end
  end

 #省略

end

記事を参考に画像を取得する事ができました。
active storageでの記事はあまり見かけなかったので、とても助かりました。

画像が小さくて、荒くなるとの噂でしたので、サイズが大きくなりそうな感じにしてみました。
色々見ているとモデル側で指定している方もいらっしゃいました。

config/initializers/devise.rb

  config.omniauth :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_SECRET_KEY'], :image_size => 'large'#これ
  config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
  config.omniauth :twitter, ENV['TWITTER_API_KEY'], ENV['TWITTER_API_SECRET_KEY'], callback_url: "http://localhost:3000/users/auth/twitter/callback", :image_size => 'original'#これ

ログイン認証関連の実装は、時間帯によってうまくいかなかったり、ブラウザにcookieが残っていると正しく動かない時がありましたので気長にやると良さそうです。

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

Rails6 OmniAuth twitter認証 emailの条件分岐

永遠の初心者による自分のためのメモ。
Rails学習開始3ヶ月。
とりあえずこれで動きました程度に考えていただければ幸いです。
こうした方がええやんと言うご意見がある方は優しいコメント頂けると嬉しいです。

この記事の目的

twitter認証はメールアドレスを取得しないので、ランダムメールアドレスを生成する。
google、facebook認証は、正規のメールアドレスを取得する。
twitterはランダムアドレス。他は、正規のアドレスで条件分岐させる。

環境

Ruby 2.7.1p83
Rails 6.0.3.3

前提条件


deviseによるログイン機能実装済み。


OmniAuthによるTwitter、google、facebookなどのログイン認証機能実装済み。


私は、以下の記事を参考にさせて頂きました


・手続き関連
https://qiita.com/kazuooooo/items/47e7d426cbb33355590e
・OmniAuth導入
https://qiita.com/LuckHackMahiro/items/9dfca6e67777a2161240

条件分岐 : TwitterかTwitter以外

機能実装できている前提で、

app/models/user.rb

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.provider = auth.provider
    user.uid = auth.uid
    user.name = auth.info.name
    user.password = Devise.friendly_token[0, 20]
    user.email = auth.info.email    #google,facebookの時
    user.email = User.dummy_email(auth) if user.provider == "twitter"   #twitterの時
    avatar = open("#{auth.info.image}")
    user.image.attach(io: avatar, filename: "user_avatar.jpg")
  end
end

としたら、twitterではランダムアドレス。
google、facebookでは正規のアドレスが取得する事ができました。

条件分岐がうまくいって嬉しかったです。

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