20200326のRailsに関する記事は15件です。

Rails開発におけるRSpecの使用方法と基礎的な理解 〜その1〜

はじめに

当記事は以下のような方の参考になるかと思います。

  • Ruby on Rails初学者
  • テストについて学びたい方
  • テストについて一次情報を確認したが、あまり理解できなかった方
  • RSpecの基礎について学びたい方
  • RSpecを簡潔に書きたい方

執筆の理由

私はプログラミングの学習にスクールを利用しましたが、テストは他の項目と比較して理解が進んでいない方が多いような印象でした。(私を含め)
その為、自身の理解が進んだ際には、記事を投稿する事でアウトプットでより理解を深めると共に、初学者の方々の理解の助けになれればと考えていました。
これが記事投稿の理由です。

ちなみに、自身がテストについて理解が進んでいなかった理由としては、以下の3点が挙げられます。

  • テストを軽視していた。
  • カリキュラム全体を確認した結果、進捗に直接の影響を与えないと考えた。
  • その性質上、DRY(Don't Repeat Yourself「何度も同じ事を記述せず、効率的にコーディングする」の意)にすることがやや制限される為、面倒に感じた。

数ヶ月前の理解が進んでいない状態の私自身に向けて、用語解説も含めて書いていきますので、おそらく大半の方が基礎中の基礎を理解出来るようになるかと思います。

開発環境と前提

Ruby : 2.5.1
Rails : 5.2.3

なお、今回はユーザー登録機能のあるアプリケーションを想定し、Userモデルに対してのテストを行っていきます。
gem 'devise' を使用しています。

手順

大きく分けて以下の4手順です。
1.gemのインストール
2.各ファイルの作成
3.テストコードの記載
4.テストを行う

gem 'rspec-rails' のインストール 〜 ターミナル操作

gem 'rspec-rails' のインストール

まずはRails内でRSpecを使用する為に、gem 'rspec-rails' をインストールします。
Gemfileに記載する際は場所に注意してください。
テスト用のgemなので、group :development, test do 〜 end内に記載します。

Gemfile
#省略

#記載する箇所に注意
group :development, :test do
  gem 'rspec-rails'
end

#省略

上記のように記載できたら、
ターミナル(著者はmacを使用しています。windowsでしたらコマンドプロンプトでしょうか?)操作を行います。

ターミナル操作

「bundle install」を実行。

ターミナル
$ bundle install

インストールできたら、
「rails g rspec:install」を実行してください。

ターミナル
$ rails g rspec:install

上記のコマンドで、rspec関連のファイルが自動作成されます。
上手くいけば、おそらく以下のような結果がターミナルに表示されるかと思います。

ターミナル
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

これで 'rspec-rails'の導入は完了しました。

テストコードを書くファイルの作成

テストコードは先ほど自動作成された「spec」内に新たにディレクトリ(≒フォルダ)、ファイルを作成し、そこに記載していきます。
今回は、Userモデルに記載のバリデーションをテストする為、spec直下に「models」というディレクトリを作成し、さらにその直下にuser_spec.rbというファイルを作成します。
(他にもモデルテストを行いたい場合は、modelsディレクトリ直下に「モデル名_spec.rb」というファイルを用意します。以下参照)

spec/models/user_spec.rb
spec/models/モデル名_spec.rb
#例です。今回こちらのファイルは使用しません。

今回テストするバリデーションの確認

前提としてdeviseを導入済みなので、わざわざ記載しなくてもバリデーションがかかっている箇所もありますが、今回は理解の助けになるようあえて記載します。
自身のアプリの使用上「name」が「nickname」になっていたりしますが、特に結果に影響を与えないので、気にしないで下さい。

app/models/user.rb
class User < ApplicationRecord
#省略

  validates :nickname, presence: true, length: { minimum: 4 }

#省略
end

今回は、
nickname

  • presence: true・・・空欄は認めない
  • length: { minimum: 4 }・・・最小文字数4文字

以上のバリデーションをかけてみます。

テストコードの記載

例として、先程spec配下に作成したファイルに'nicknameが空欄の場合登録できない'テストコードを書いてみます。

spec/models/user_spec.rb
require 'rails_helper'      #require 〜  ・・・rubyのメソッド。外部ファイルの読み込みを行う。

describe User do
  describe '#create' do
     it 'nickname空欄の場合登録できない' do
       user = User.new(
         nickname: "", 
         email: "aaa@gmail.com", 
         password: "000000", 
         password_confirmation: "000000"
         )
       user.valid?
       expect(user.errors[:nickname]).to include("can't be blank", "is too short (minimum is 4 characters)")
     end
  end
end

最上部に「require 'rails_helper'」とありますが、これは「rails g rspec:install」をした際に自動作成された「rails_helper.rb」を読み込みます、という意味です。
あらかじめrails_helper.rbに各ファイル共通の設定を記載しておき、テストを実行する際には「require 'rails_helper'」によってその共通設定を読み込むことで、共通の設定を使用します。

rails_helper.rb内のデフォルトの記載に関してのここでの解説は、割愛させていただきます。

以下、各行の説明です。

  • describe User do
    descrive 〜 do内には何を記載しても構いません。が、コードを読む人がわかりやすいように書く必要があります。
    この場合Userモデルについての内容の為、「User」と記載してあります。
    「これからUserモデルについて書きますよ」という宣言のようなものです。

  • descrive '#create' do
    同じくdescrive 〜 do内に何を記載しても構いません。
    この場合、直上のコードと合わせて「これから(Userを)createする際の事を書きますよ」といった宣言をする内容になっています。
    ちなみに、createの前に#がついていますが、これはメソッド名を記載する際にはつけるという慣習に倣って記載しています。
    記載者以外がコードを読む際の可読性を向上させる為のものだと理解しています。

  • it 'nicknameが空欄の場合登録できない' do
    it 〜 do内にテストの内容をわかりやすく記入します。

(私なりにわかりやすく記載したつもりですが、実務で行う際は各企業のやり方に従ってください。個人開発レベルでは問題ないかと思われます。)

  • user = User.new(〜)
    この行ではテストにかける新規のインスタンスを作成しています。
    User.new(〜)でカッコ内の情報を持つインスタンスを作成し、左辺のuserに代入しています。
    今回は「nicknameが空欄の場合 登録できない 」事を確かめたいので、カッコ内のnicknameを "" として、あえて空欄にしています。
    このnicknameが空欄のuserをテストにかけていきます。

  • user.valid?
    valid?で先程作成したuserがバリデーションにかかるかどうか(実際に保存できる内容かどうか)を判定します。
    バリデーションの内容は、
    presence: true、つまり「空欄は認めない」というものですので、バリデーションにかかることが予想されます。

  • expect(user.errors[:nickname]).to include("can't be blank", "is too short (minimum is 4 characters)")
    そして、その予想を書くのがここです。エクスペクテーションといいます。
    構文としては、expect(X).to マッチャ(Y)です。(マッチャは後述で説明します)
    昔、英語の学習をする際にexpect 人 to do〜「人が〜するのを期待する/人に〜するよう要望する」というのを習った気がしますが、それに非常に似た形で作られているかと思います。というかそのままでしょうか。

今回の場合は、user(先程作成した新規インスタンス)のnicknameカラムに関してのエラーメッセージにcan't be blankis too short (minimum is 4 characters)含まれるというエクスペクテーションになっています。

細かく見ていくと、

  • user.errors[:nickname]

まずuserは先程作成したuserです。
errorsはメソッドで、ここでは先程のuser.valid?でfalseの場合の「理由=エラーメッセージ」を作成します。
よくわからない場合は、ターミナルでコンソールを立ち上げると理解が深まるかと思います。

ターミナル
---コンソール立ち上げ---
$ rails c
Running via Spring preloader in process 46362
Loading development environment (Rails 5.2.3)

---インスタンス生成&userに代入---
[1] pry(main)> user = User.new(nickname: "", email: "aaa@gmail.com", password: "000000", password_confirmation: "000000")
   (0.8ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
=> #<User id: nil, nickname: "", email: "aaa@gmail.com", workplace: nil, created_at: nil, updated_at: nil, image: nil>

---user.valid?の実行---
[2] pry(main)> user.valid?
  User Exists (84.4ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'aaa@gmail.com' LIMIT 1
  User Exists (0.6ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'aaa@gmail.com' LIMIT 1
=> false
---直上の行でfalseが出ている。作成したuserがバリデーションにかかっていることがわかる。---

---user.errorsの実行---
[3] pry(main)> user.errors
=> #<ActiveModel::Errors:0x00007fc2da5852d8
 @base=#<User id: nil, nickname: "", email: "aaa@gmail.com", workplace: nil, created_at: nil, updated_at: nil, image: nil>,
 @details={:nickname=>[{:error=>:blank}, {:error=>:too_short, :count=>4}]},
 @messages={:nickname=>["can't be blank", "is too short (minimum is 4 characters)"]}>

最後の行を見てみると、
messages={:nickname=>["can't be blank", "is too short (minimum is 4 characters)"]}
とありますが、これがバリデーションにかかった理由、エラーメッセージです。。

nicknameに関して、

  • "can't be blank"・・・「空欄にできない」
  • "is too short (minimum is 4 characters)"・・・「最小文字数4文字」

とあります。
テストコードで書いた通りの内容になっているので、このテストは通過するはずですね。

  • .to〜

「〜であること(期待する)」という意味になるメソッドです。ただ、私が確認した限り構文内のexpect(X)の後に必ず付くものなので、取り急ぎは必ず付けるとしていた方が良いかもしれません。
ちなみに、「〜でないこと(を期待する)」場合には.not_to〜もしくは.to_not〜を利用します。

  • include

この部分をマッチャと言い、この後ろに記述する内容expect(X)のカッコ内の関連性について記述します。
includeもマッチャの一つで、「〜を含む」という内容です。
この他にもeq「等しい」be_valid「バリデーションにかからない」などがありますが、こういった内容に関しては他記事に譲ることにします。

  • ("can't be blank", "is too short (minimum is 4 characters)")

ターミナルで確認出来る、バリデーションにかかった際のメッセージです。

実際にテストにかけてみる

まずは以下のコードを$ rails g rspec:installをした際に作成された.rspecファイルに追記します。

--format documentation

これを記載することによって、テストにかけた際のメッセージが理解しやすいものになります。

.rspec
--format documentation

次に、ターミナルでbundle exec rspecを実行します。
これが実際にテストにかける為のコマンドです。

ターミナル
$bundle exec rspec

すると、以下のように出力されるかと思います。

ターミナル
$bundle exec rspec
#省略

User
  #create
    nickname空欄の場合登録できない

Finished in 0.16438 seconds (files took 11.21 seconds to load)  #この行は環境によって異なります。(かかった時間を表示)
1 example, 0 failures

最下部のexampleとfailerはそれぞれ「実行したテストの数」と「テストが通過しなかった数について」表示しています。
よって、今回はテストが通過したという結果になりました。

ちなみに、、、

テストが通過しない場合は以下のような出力になります。
(安直ですが、今回はエクスペクテーション内の.to.not_toに書き換えて意図的に通過しないテストコードを書きました。)

spec/models/user_spec.rb
#書き換え部分
expect(user.errors[:nickname]).not_to include("can't be blank", "is too short (minimum is 4 characters)")

ターミナル
$bundle exec rspec
#省略

User
  #create
    nickname空欄の場合登録できない (FAILED - 1)

Failures:

  1) User#create nickname空欄の場合登録できない
     Failure/Error: expect(user.errors[:nickname]).not_to include("can't be blank", "is too short (minimum is 4 characters)")
       expected ["can't be blank", "is too short (minimum is 4 characters)"] not to include "can't be blank" and "is too short (minimum is 4 characters)"
     # ./spec/models/user_spec.rb:11:in `block (3 levels) in <top (required)>'

Finished in 0.15455 seconds (files took 6.67 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/models/user_spec.rb:7 # User#create nickname空欄の場合登録できない

今回は1つしかテストにかけていませんが、先程と違い1 example, 1 failureと出力されています。
テストに通過していないということですね。
また、Faileresということで、テストにかけた際に通過しなかったエクスペクテーションが具体的にどのテストなのかが1),2)・・・と一覧表示されます。

この結果をもってテストコードの内容が間違っているのか、スペルミスがあるのか、テストコードが間違っていないのであれば、そもそもの仕様に問題があるのかという検証に入るのかと思われます。

編集後記

今回の内容は私が通っているスクールのカリキュラム内容に沿ったものになっているのですが、やや疑問を感じ自身で調べたところ、登録できない事を確かめるエクスペクテーションに
expect(user.save).to be_falseyというものが出てきました。
(userは当記事の内容に沿った表記としています)

つまり、nicknameが空のuserは保存(登録)できない。という内容です。
まさに今回書きたい内容ですし、より簡潔にかけるなぁと思ったんです。

これでいいのでは?とスクールのメンターに質問したところ、「今回は保存できないことが明示的で、後にコードを見返した時に保存できない理由がひと目でわかるように書く必要がある」との事でした。
実務ではそういった意識が必要であることを痛感しました。

最後に

factory_botというgemを利用する事でより簡潔にテストコードを書く方法があります。
その内容については、また別の記事で書いていこうと思います。

今回が初めての投稿なので不備があるかもしれませんが、ご容赦ください。
出来る限り「備忘録」的な内容でなく、他の初学者の方が読んだ際の参考になるよう書いてきました。
どこかの誰かのお役に立てることを切に願います。

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

accepts_nested_attributes_forを理解する

Railsのaccepts_nested_attributes_forというヘルパーについて、日本語で分かりやすい記事がなかなか見つからないのでアウトプットしておきます。
特に関係は無いですが、viewファイルはhamlで記載していきます。

環境

ruby 2.6.5
Rails 5.2.4.2

概要

そもそもaccepts_nested_attributes_forとは何なのか?
Rail APIガイド
一番上の解説をそのまま翻訳してみます。

入れ子になった属性を使用すると、親を介して関連するレコードに属性を保存することができます。デフォルトでは、入れ子になった属性の更新はオフになっており、accept_nested_attributes_forクラス・メソッドを使用して有効にすることができます。入れ子になった属性を有効にすると、モデル上に属性ライターが定義されます。

属性ライターの名前はアソシエーションにちなんで付けられます。

入れ子になった属性Nested attributesと書かれているのでそういう名称だと捉えておきましょう。
要点を絞ると「親を介して関連するレコードに属性を保存することができる」ようです。
これだけだと分かりにくいですが、すぐ下のOne-to-oneのところにあるコードを読むと何となく意味が分かります。

member.rb
class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar
end
controller
params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } }
member = Member.create(params[:member])
member.avatar.id # => 2
member.avatar.icon # => 'smiling'

Memberモデル(親)にAvatarモデル(子)がネストしていて、Member.createの処理だけでAvatarの保存が完了しています(member.avatar.idがある=avatarのレコードが作成されている)

つまりモデルAの保存時に、それに紐づくモデルBをまとめて保存できるというのがこのヘルパーの役割のようです。

モデルの実装

teamモデルに多数のmemberが属している想定でコードを記載していきます。
モデルを作るとかルーティングを書くとか本記事に関係無い部分は省略してます。

モデルではアソシエーションの記述に加えて、親モデルに以下の記述を追記します。

team.rb
has_many :members
accepts_nested_attributes_for :members

この記述によって、Teamモデルのレコード保存時にMemberモデルのレコードをまとめて保存ができるようになります。
また、デフォルトで更新はできるけど削除できず、削除したかったらallow_destroyオプションをつけるようです。

team.rb
accepts_nested_attributes_for :members, allow_destroy: true

Memberモデル側はbelongs_to :teamとアソシエーションを書くだけで完了です。

ビューの実装

ビューではfields_forというヘルパーを使い、team用のform_with(またはform_for)の中にmembers用の入力フィールドを用意します。
Railsガイド

new.html.haml
= form_with model: @team, local: true do |f|
  %div team用のフォーム
  = f.label :name
  = f.text_field :name
  %hr
  ここからmembers
  = f.fields_for :members do |m|
    %div
      = m.label :nickname
      = m.text_field :nickname
    %div
      = m.label :age
      = m.number_field :age
  ここまでmembers
  %hr
  = f.submit

このように@team用のform_withの中にmembers用のfields_forが入っている形で記述します。

as1.jpg
しかしこのフォームには問題点があります。コントローラで@teamを作ってみましょう。

controller
def new
  @team = Team.new
end

するとなぜかmembers用のフィールドが消えてしまいました・・・
as2.jpg
実はfields_forというのは親モデルに紐づく子モデルの数だけ回るeach文のような動きをします。
今回の場合だと@team.membersの数だけfields_forの中身は実行されるということです。
@teamを作ったことでフォームが適切に機能し始め、@team.membersが無いのでfields_forの中身が実行されなかったということです。
Railsガイドにもコントローラで最低1つの子モデルのインスタンスを作成しておくのが常套手段だと書かれています。

controller
def new
  @team = Team.new
  @team.members.new
end

こうしておくとmembers入力用のフィールドが最初から1つ生成された状態になります。
timesメソッドなどを使って複数回@team.members.newを実行すれば、その数だけfields_forが入力フィールドを生成してくれます。

コントローラの実装

コントローラの実装は非常にシンプルです。
@teamを保存する処理と、fields_forの値を受け取ることができるストロングパラメータを実装すれば完成です。

controller
  def create
    @team = Team.new(team_params)
    if @team.save
      redirect_to root_path
    else
      render :new
    end
  end

  private
  def team_params
    params.require(:team).permit(:name, members_attributes: [:nickname, :age])
  end

ストロングパラメータの形が独特です。
これを理解するために、ビューから送られてくるparamsがどうなっているか見てみましょう。

params
"team"=>{
  "name"=>"Tema1",
  "members_attributes"=>{
    "0"=>{
      "nickname"=>"Member1",
      "age"=>"22"
    }
  }
}

teamの中にmembers_attributesがネストして、
その中に"0"がネストして、
さらにその中にmembers用のパラメータが入っています。
ここでRailsガイドの解説を引用します

:addresses_attributesハッシュのキーはここでは重要ではありません。各アドレスのキーが重複していなければそれでよいのです。

この記事ではaddressesではなくmembersですが、要するに"0"の部分はmembersが増えるにつれて重複なく設定できれば何でも良いということですね。
そう考えれば"0"を無視して、送られてくるparamsとストロングパラメータの形(ネストの仕方)が一致していることが分かります。

ここまでの実装で、実際にフォームを使って送信してみると・・・
as3.jpg
@team.saveしか書いてないのにMemberもcreateされてます!
membersテーブルのteam_idカラムにもきちんと今作ったteamのidが入っています。

編集できるようにする

ここでRailsガイドもAPIリファレンスも解説が少なく、途方に暮れそうですが実装は至ってシンプルです。
まずはedit用のビューファイルを作成し、newと使い回せるようにしておきます。

new.html.haml
= render "form"
edit.html.haml
= render "form"
_form.html.haml
= form_with model: @team, local: true do |f|
  %div team用のフォーム
  = f.label :name
  = f.text_field :name
  %hr
  ここからmembers
  = f.fields_for :members do |m|
    %div
      = m.label :nickname
      = m.text_field :nickname
    %div
      = m.label :age
      = m.number_field :age
  ここまでmembers
  %hr
  = f.submit

コントローラもRailsの基本的なものを書いておきます。

controller
  def edit
    @team = Team.find(params[:id])
  end

  def update
    @team = Team.find(params[:id])
    if @team.update(team_params)
      redirect_to root_path
    else
      render :edit
    end
  end

ここで既存レコード更新について、Rails APIリファレンスから引用します。
このリファレンスではmemberが親モデルでpostsが子モデルです。

If the hash contains an id key that matches an already associated record, the matching record will be modified:

member.attributes = {
  name: 'Joe',
  posts_attributes: [
    { id: 1, title: '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
    { id: 2, title: '[UPDATED] other post' }
  ]
}

member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
member.posts.second.title # => '[UPDATED] other post'

要するにposts_attributesの中にidというキーが含まれていて、そのidのレコードが実際にpostsテーブルにあれば更新される、と。

そして今度はRailsガイドから引用します。

関連付けられたオブジェクトが既に保存されている場合、fields_forメソッドは、保存されたレコードのidを持つ隠し入力を自動的に作成します。

関連付けられたオブジェクトが既に保存されている場合=>つまりeditでは
保存されたレコードのidを持つ隠し入力を自動的に作成します=>APIリファレンスで必要と書かれていたidキー用のinputを自動生成します。

実際にeditの画面で検証ツールを開くと、作った覚えの無いinputができてます。
as4.jpg
必要なキーが自動生成されてるってことはもしかしてもう実装終わり?Rails凄すぎない?
と思って送信してみたら期待とは違ったことになりました。
as5.jpg
Teamはupdateですが、Memberはcreateとなっています。実際に既存レコードの更新ではなく1件増えてしまいました。
しかし、こうなってしまった原因もこの中に書いていますね。
Unpermitted parameter: :id
idというキーでparamsは送られてきたが、permitできなかった。
つまりストロングパラメータでidを許可していないことが原因です。

controller
def team_params
  params.require(:team).permit(:name, members_attributes: [:nickname, :age, :id])
end

:ageの後ろに:idを追加しました。
そして再度editからフォームを送信してみると・・・
as6.jpg
Memberもupdateになりました!やっぱりRails凄すぎない?(10分ぶり2回目)
実際にDBの値を確認すると更新されていたのでこれで実装完了です。

削除できるようにする

Railsガイドに丁寧に書いてあったのでそのまま引用&実装していきます。

accepts_nested_attributes_forにallow_destroy: trueを渡すと、関連付けられたオブジェクトをユーザーが削除することを許可できるようになります。

これはモデルの実装で書いたので大丈夫ですね。

あるオブジェクトの属性のハッシュに、キーが_destroyで値が1またはtrueの組み合わせがあると、そのオブジェクトは削除されます。

要するに_destroyというキーの入力欄を作り、1かtrueが送られてくれば削除できる、と。
数字の入力欄でも良いですが、公式に倣いチェックボックスで作ってみます。

_form.html.haml
= f.fields_for :members do |m|
  %div
    = m.label :nickname
    = m.text_field :nickname
  %div
    = m.label :age
    = m.number_field :age
# ここから下を追記
  %div
    = m.check_box :_destroy

updateの時の失敗でレコードが2件に増えてますが、fields_forが2周動いて入力欄とチェックボックスが2個ずつ生成できているのが分かります。
これにチェックを入れればそのmemberは削除される、という寸法です。
as7.jpg
そして最後に、ストロングパラメータに_destroyを追加します。
updateの時の失敗は繰り返さない!

controller
  def team_params
    params.require(:team).permit(:name, members_attributes: [:nickname, :age, :id, :_destroy])
  end

試しにチェックボックスにチェックを入れて送信してみると・・・
as8.jpg
削除されました。Rails凄すぎない?(20分ぶり3回目)
削除機能も実装完了です。

編集画面で新規追加もできるようにする

今のままだとeditページでは新規メンバーの追加ができません。
既存のteamに新しいメンバーを追加するためのフィールドも用意したいところです。

fields_forは子レコードの数だけ回るeach文だと最初の方で書きました。
editでは子レコードの数=既存メンバーの数なので、追加でもう1つインスタンスを作っておけば良さそうですね。

controller
def edit
  @team = Team.find(params[:id])
  @team.members.new # ここを追記
end

editの定番処理@team = Team.find(params[:id])に加えて、newの時に書いたmemberのインスタンスを作る処理を追加しました。
as9.jpg
既存メンバーが1人のteamのeditでこのようになりました。いい感じです。
as10.jpg
実際に既存メンバーのupdate+新規メンバーのcreateができています。解決!

...と思ったのですが。こんな落とし穴がありました
as11.jpg
editで新規メンバーを追加したくないので空けたまま送信します。
as12.jpg
名無しさんがcreateされてしまいました。
これは良くないですね。バリデーションをきちんと実装した場合、そもそも保存に失敗しそうです。
どうしようかと思ったらRailsガイドに書いてありました。

ユーザーが何も入力しなかったフィールドを無視できれば何かと便利です。これは、:reject_if procをaccepts_nested_attributes_forに渡すことで制御できます。このprocは、フォームから送信された属性のハッシュ1つ1つについて呼び出されます。このprocがfalseを返す場合、Active Recordはそのハッシュに関連付けられたオブジェクトを作成しません。

※falseと書いてますが参考コード的にtrueだと思われます

procというのはブロックのようなものです(厳密には違います)
つまり
1. accepts_nested_attributes_for
2. reject_ifオプションを追加し、
3. 空だったらtrueを返す式をproc(ブロック)で書いておく
以上で、空のレコードが生成されないようにできるようです。
ブロックの渡し方は色々あるようですが、個人的にメソッド化する方法がしっくり来たのでその方針で実装します。
Rails APIガイドreject_ifで検索すると書き方がいくつか載ってます)

team.rb
accepts_nested_attributes_for :members, allow_destroy: true, reject_if: :reject_members

private
def reject_members(attributes)
  attributes[:nickname].blank? || attributes[:age].blank?
end

reject_membersメソッドを実装し、:nicknameもしくは:ageが空だったらtrueを返すようにしました。
今回はnicknameとageの両方を入力して欲しいのでこの形にしています。
メソッド化しておけばこの辺りの条件をカスタマイズする時に読みやすそうなのでこの方法を選択しました。
as13.jpg
これでageに100を入力すると、paramsは送信できていますがmemberのcreateはされず・・・
as14.jpg
nicknameとageが揃っていればmemberがcreateされるようになりました!
最低限やりたいことが実装できたため、今回の実装はここまでにします。

終わりに

この先はチェックボックスを消した状態で操作したり、新規追加用のフィールドを動的に追加したり・・といったことをJSで実装していくケースが多いかと思います。
本記事は一切JSに触れていませんが、結局JSで書くべきコードはaccepts_nested_attributes_forを使うための操作をJSで実現する、というだけです。
ここの理解を固めてからJSを書かないと、エラーが出てもデバッグが進まないと思います。

そしてここまで書いて気づきましたが、APIリファレンスとRailsガイドで完結してしまいました。
公式で完結するから参考記事が少ないんでしょうね。やっぱり一次ソースは大切です。

参考サイト

Rails APIリファレンス
Railsガイド

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

Ruby単体でDB接続したいとき(MySQL)

はじめに

RubyでDBに接続したい時があるかと思います。

例えばスクレイピングしてきた情報をDBに格納したい時など。

ActiveRecordを使うと実装できます。

ActiveRecordはRailsに搭載されているので、皆さんご存知かとは思いますが、

ACtiveRecordを単体で使ったことがない方は結構いらっしゃるかと思いますので、ご紹介します。

今回はMySQLを使います。

ActiveRecordを単体で使ってみる

準備

まずはDBを用意しましょう。MySQLにログインします。

mysql -u root

データベースを作ります。名前は今回sampleにします。

mysql> create database sample;

sampleデータベースに入ります。

mysql> use sample;

テーブルを作ります。名前は今回usersにします。Rubyの規約上、テーブル名は複数形にしてください(sをつける)

mysql> create table product (id int auto_increment not null primary key, name varchar(10));

一旦抜けましょう

mysql> exit

続いてActiveRecordをインストールしましょう

$ gem install activerecord

mysql2もインストールします

$ gem install mysql2

実装

これで準備ができたのでRubyのファイルを作ります

$ vi sample.rb

avtive_recordのgemを呼び出し、DBの接続設定を以下のように記します

sample.rb
# ↓ ファイルに書く時はアンダースコアがいるので注意
require 'active_record' 

#DB接続設定
 ActiveRecord::Base.establish_connection( 
  adapter:  "mysql2", 
  host:     "localhost", #ローカルのDBに接続します。
  username: "root", #ユーザー名
  password: "",  #設定したMySQLのパスワード
  database: "sample",  #接続したいDB名
)

更にUserクラスを設定します。これでDBのusersテーブルがいじれるようになります

sample.rb
require 'active_record' 

 ActiveRecord::Base.establish_connection( 
  adapter:  "mysql2", 
  host:     "localhost", #ローカルのDBに接続します。
  username: "root", #ユーザー名
  password: "",  #設定したMySQLのパスワード
  database: "sample",  #接続したいDB名
)

#以下追記
class User < ActiveRecord::Base
end

試しにUserテーブルにレコードを作りましょう

sample.rb
require 'active_record' 

 ActiveRecord::Base.establish_connection( 
  adapter:  "mysql2", 
  host:     "localhost", #ローカルのDBに接続します。
  username: "root", #ユーザー名
  password: "",  #設定したMySQLのパスワード
  database: "sample",  #接続したいDB名
)

class User < ActiveRecord::Base
end

#以下追記
User.create(name: "taro")

実行してみます

$ ruby sample.rb

ファイルを変更してレコードができたか確認しましょう

sample.rb
require 'active_record' 

 ActiveRecord::Base.establish_connection( 
  adapter:  "mysql2", 
  host:     "localhost", #ローカルのDBに接続します。
  username: "root", #ユーザー名
  password: "",  #設定したMySQLのパスワード
  database: "sample",  #接続したいDB名
)

class User < ActiveRecord::Base
end

#以下変更
puts User.find_by(name: 'taro').name

実行

$ ruby sample.rb

以下のように出ればOKです。

taro

終わりに

今回は一つのファイルの中に設定など全て書きましたが、実際に使う時は、DB接続設定を他のRubyファイルに書いてrequireするか、もしくは、YMLファイルに設定を書いて読み込んだりなどして使ってみてください。

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

Ruby単体でDB接続したい時(MySQL)

はじめに

RubyでDBに接続したい時があるかと思います。

例えばスクレイピングしてきた情報をDBに格納したい時など。

ActiveRecordを使うと実装できます。

ActiveRecordはRailsに搭載されているので、皆さんご存知かとは思いますが、

ACtiveRecordを単体で使ったことがない方は結構いらっしゃるかと思いますので、ご紹介します。

今回はMySQLを使います。

ActiveRecordを単体で使ってみる

まずはDBを用意しましょう

mysql -u root

データベースを作ります。名前は今回sampleにします。

mysql> create database sample;

sampleデータベースに入ります。

mysql> use sample;

テーブルを作ります。名前は今回usersにします。Rubyの規約上、テーブル名は複数形にしてください(sをつける)

mysql> create table product (id int auto_increment not null primary key, name varchar(10));

一旦抜けましょう

mysql> exit

続いてActiveRecordをインストールしましょう

gem install activerecord

mysql2もインストールします

gem install mysql2

続いてrubyのファイルを作ります

$ vi sample.rb

avtive_recordのgemを呼び出し、DBの接続設定を以下のように記します

sample.rb
# ↓ ファイルに書く時はアンダースコアがいるので注意
require 'active_record' 

#DB接続設定
 ActiveRecord::Base.establish_connection( 
  adapter:  "mysql2", 
  host:     "localhost", #ローカルのDBに接続します。
  username: "root", #ユーザー名
  password: "",  #設定したMySQLのパスワード
  database: "sample",  #接続したいDB名
)

更にUserクラスを設定します。これでDBのusersテーブルがいじれるようになります

sample.rb
require 'active_record' 

 ActiveRecord::Base.establish_connection( 
  adapter:  "mysql2", 
  host:     "localhost", #ローカルのDBに接続します。
  username: "root", #ユーザー名
  password: "",  #設定したMySQLのパスワード
  database: "sample",  #接続したいDB名
)

#以下追記
class User < ActiveRecord::Base
end

試しにUserテーブルにレコードを作りましょう

sample.rb
require 'active_record' 

 ActiveRecord::Base.establish_connection( 
  adapter:  "mysql2", 
  host:     "localhost", #ローカルのDBに接続します。
  username: "root", #ユーザー名
  password: "",  #設定したMySQLのパスワード
  database: "sample",  #接続したいDB名
)

class User < ActiveRecord::Base
end

#以下追記
User.create(name: "taro")

実行してみます

ruby sample.rb

ファイルを変更してレコードができたか確認しましょう

sample.rb
require 'active_record' 

 ActiveRecord::Base.establish_connection( 
  adapter:  "mysql2", 
  host:     "localhost", #ローカルのDBに接続します。
  username: "root", #ユーザー名
  password: "",  #設定したMySQLのパスワード
  database: "sample",  #接続したいDB名
)

class User < ActiveRecord::Base
end

#以下変更
puts User.find_by(name: 'taro').name

実行

ruby sample.rb

以下のように出ればOKです。

taro

終わりに

今回は同じファイルの中に設定など全て書きましたが、実際に使う時は、DB接続設定を他のRubyファイルに書いてrequireするか、もしくは、YMLファイルに設定を書いて読み込んだりなどして使ってみてください。

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

Rails(6.02) + PostgreSQL(12.2) の環境をdocker-composeで作成する(webpacker対応版)

はじめに

Rails+PostgreSQLの環境をdocker-composeで作成する - Qiitaで紹介されている方法はRails5での動作を前提としているようでした。
Rails6のwebpackerまわりの記述を追加し、アップデートしたのがこの記事になります。

Version

  • Ruby 2.7.0
  • Rails 6.0.2 (Dockerfileでバージョン固定します)
  • PostgreSQL 12.2

ファイル構成

元記事におなじです。

./
|- app/  //共有フォルダ
|- docker-compose.yml
|- web/
   |- Dockerfile

Dockerfileの作成

Dockerfile
FROM ruby:2.7.0

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
  && apt-get update -qq \
  && apt-get install -y build-essential libpq-dev postgresql-client \
  && apt-get install -y nodejs yarn

RUN gem install rails -v "6.0.2"
RUN mkdir /app
WORKDIR /app

Dockerfileの変更点

  • Rubyのバージョンアップ FROM ruby:2.7.0
  • Railsのバージョン固定 RUN gem install rails -v "6.0.2"
    • 元記事でもおなじようにバージョン固定(-v "5.2.3"とか)すればそのまま使えるはず(未調査)
  • webpacker用のライブラリ(yarnとnodejs)の追加
yarnとnodejsの追加部分
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
  && apt-get update -qq \
  && apt-get install -y nodejs yarn

docker-compose.ymlの作成

docker-compose.yml
version: "3"

services:
  web:
    build: web
    ports:
      - "3000:3000"
    environment:
      - "DATABASE_HOST=db"
      - "DATABASE_PORT=5432"
      - "DATABASE_USER=postgres"
      - "DATABASE_PASSWORD=admin"
    links:
      - db
    volumes:
      - "./app:/app" #共有フォルダの設定
    stdin_open: true

  db:
    image: postgres:12.2
    ports:
      - "5432:5432"
    environment:
      - "POSTGRES_USER=postgres"
      - "POSTGRES_PASSWORD=admin"

docker-compose.ymlの変更点

  • postgreSQLのバージョンアップ image: postgres:12.2

コンテナを起動する

docker-compose build
docker-compose up -d

コンテナにログインし、Railsアプリを作成

docker-compose exec web bash
rails new <アプリ名> -d postgresql -BT

Gemfile

Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.7.0'

gem 'rails', '~> 6.0.2'
gem 'pg', '>= 0.18', '< 2.0'
gem 'puma', '~> 4.1'
gem 'sass-rails', '>= 6'
gem 'webpacker', '~> 4.0'
gem 'turbolinks', '~> 5'
gem 'bootsnap', '>= 1.4.2', require: false

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

config/database.yml

アプリ名は「yay」にしています。

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: <%= ENV.fetch('DATABASE_HOST') { 'localhost' } %>
  port: <%= ENV.fetch('DATABASE_PORT') { 5432 } %>
  username: <%= ENV.fetch('DATABASE_USER') { 'root' } %>
  password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>

development:
  <<: *default
  database: yay_development

test:
  <<: *default
  database: yay_test

production:
  <<: *default
  database: yay_production
  username: yay
  password: <%= ENV['YAY_DATABASE_PASSWORD'] %>

Railsアプリを起動する

作成したアプリのディレクトリに移動して、もろもろのセットアップをおこなえばアプリが起動できるはずです :smile:

cd <アプリ名>
bundle install
rails webpacker:install
rails db:create
rails db:migrate
rails s -b 0.0.0.0 -d 

サンプルレポジトリ

https://github.com/hirocueki/docker-rails

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

SQLに関する基本をまとめさせていただきました

基本

ローカル環境ではバックでMySQLが動いている状態

サーバーに接続

dbを作る

table(行列)としてデータを作る

アプリ1つにつき1つデータベースがあり、
データベースの上にはテーブルが複数設置できる

データベース毎に取り扱えるユーザーを限定するのが基本

命令を書いたファイルをターミナルで指定して実行するのが主流
その命令にはdbを作るやtableを作るやデータ入力をする命令を書ける
またそこにはデータ入力時の条件も書ける
ex.一意性、 デフォルト値、 空を許さないなど

viewとはtableにいくつも持てる物で、特定の条件を満たしたtableという感じ
tableが条件付きtableを何個も持てるので、見返す時に便利

トランザクション
→まとめて行いたい処理をまとめて行えるようにする
ex. 2つのデータ更新をまとめて行いたい時

特定のカラムに索引、インデックスをつけるとデータ抽出が速くなる
しかしいろんなカラムにつけると、その処理に時間がかかり元も子もないのでおすすめはしない

複数のテーブルの紐付け
Aテーブルの外部キーとBテーブルの主キーが一致していることが必要になる。
Join onで表示可能

トリガーとは
あるtableで入力や削除や変更がされたら、トリガーでという別の場所でその記録をを保存できる

クエリとサブクエリとは
クエリとはデータを読み出す命令のこと。
サブクエリとは、読み出した値を条件に入れてデータを読み出すために使う命令

わざわざデータから新しく抽出した値をカラムとして保存しない場合に、一時的に新しい値を得る時に使う。

SQLの処理の順番

FROM:データを取ってくる

JOIN:データを統合する

WHERE:データの所得条件を絞る

GROUP:グループに分けて、その中で関数処理をして出力をする。そのためGROUPと関数はセット

関数:データにある値をいじって出力、GROUP無しでも使える

HAVING:グループ化した後にデータ所得の条件を絞る

SELECT:読み出したいデータを決める

ORDER:好きな順番に並べる

LIMIT:見れる範囲を狭める

読み出す時の注意
どういう条件で絞るのか、どういうグループ分をするのかを大切にする

またそれらの処理がどういう順番なのかも注意する

開発中に行列の形で見たい時(SQliteの時)

DB browser for sqliteをダウンロード

アプリのDBの中にあるdevelopment sqliteをダウンロードしてDB browser for sqliteで見る

add-indexでカラムにインデックスを付けること

rails g migrationで任意のカラムにインデックスを加えるマイグレーションファイルを作る(emailカラムに使われる事が多い)

rails db migrateでデータに反映
emailカラムにインデックスが付く→これによって検索が速くなる(インデックスのおかげでアルファベット順に並べ替えたりできるから)

またadd-indexマイグレーションファイルのオプションとしてunique:trueにする事で、カラムの内容は一意性でないといけなくなる(つまり同一のメルアドは登録できない)

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

Railsチュートリアル〜5章〜ふざけてる時間ないので真面目に勉強する

レイアウトを作成する。

5章目に入っていきます。4章はRailsっていうよりもRubyの基本的な文法や考え方についてやっていましたが今回からは具体的にsample_appのレイアウトを整えていく手法を勉強していきます。

ナビゲーション

まずサンプルアプリケーションにリンクとスタイルを追加しないといけない。
新たにapplication.html.erbにHTML構造を追加する。

<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                               'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                               'data-turbolinks-track': 'reload' %>
    <!--[if lt IE 9]>
      <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
      </script>
    <![endif]-->
  </head>
  <body>
    <header class="navbar navbar-fixed-top navbar-inverse">
      <div class="container">
        <%= link_to "sample app", '#', id: "logo" %>
        <nav>
          <ul class="nav navbar-nav navbar-right">
            <li><%= link_to "Home",   '#' %></li>
            <li><%= link_to "Help",   '#' %></li>
            <li><%= link_to "Log in", '#' %></li>
          </ul>
        </nav>
      </div>
    </header>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>

<!DOCTYPE html>はHTML5であることの宣言に使用する。

<!--[if lt IE 9]>
  <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
  </script>
<![endif]-->

これは、Microsoft Internet Explorer (IE) のバージョンが9より小さい場合 (if lt IE 9) にのみ、囲まれている行を実行する。これにより、Firefox、Chrome、Safariなどの他のブラウザに影響を与えずに、IEのバージョンが9未満の場合にのみHTML5を読み込める。

それ以外はprogateやってればすぐわかる内容。

<%= link_to "sample app", '#', id: "logo" %>
<nav>
  <ul class="nav navbar-nav navbar-right">
    <li><%= link_to "Home",   '#' %></li>
    <li><%= link_to "Help",   '#' %></li>
    <li><%= link_to "Log in", '#' %></li>
  </ul>
</nav>
ここで埋め込みRubyコード出現。

リンクを生成するために、Railsヘルパーのlink_toを使用。 (リンクテキスト・URLを引数としている)
このURLは名前付きルート (Named Routes) るが今はひとまずURL「'#'」を置いておく。
第3引数はオプションハッシュで、この場合はサンプルアプリのリンクでCSS id logoを指定している(他の3つのリンクにはオプションハッシュが指定されていませんが、必須ではないので構わない。)

<nav>
  <ul class="nav navbar-nav navbar-right">
    <li><%= link_to "Home",   '#' %></li>
    <li><%= link_to "Help",   '#' %></li>
    <li><%= link_to "Log in", '#' %></li>
  </ul>
</nav>
divの内側の2番目の要素は、リストアイテムタグliと
順不同リストタグulによって作られた、ナビゲーションリンクのリスト。

navタグ=「その内側がナビゲーションリストである」という意味

Railsが埋め込みRubyを評価し、レイアウトを描画するとリストが次のように置き換わる。

<nav>
  <ul class="nav navbar-nav navbar-right">
    <li><a href="#">Home</a></li>
    <li><a href="#">Help</a></li>
    <li><a href="#">Log in</a></li>
  </ul>
</nav>
これがブラウザに返される。

<div class="container">
  <%= yield %>
</div>

上と同様、containerクラスもBootstrapにおいて特別な意味を持ちます。yieldメソッドはWebサイトのレイアウトにページごとの内容を挿入する。
これでフッターを除いてレイアウトは完成する。

サインアップページへのリンクがあるHomeページ
app/views/static_pages/home.html.erb

<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>

  <h2>
    This is the home page for the
    <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    sample application.
  </h2>

  <%= link_to "Sign up now!", '#', class: "btn btn-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.png", alt: "Rails logo"),
            'http://rubyonrails.org/' %>

第7章でサイトにユーザーを追加するときに備えて、最初のlink_toで次のような仮のリンクを生成します。

<a href="#" class="btn btn-lg btn-primary">Sign up now!</a>

2番目のlink_toでは、引数として画像ファイルのパスと任意のオプションハッシュをとるimage_tagヘルパーの能力が示されています。このヘルパーでは、シンボルを使ってalt属性などを設定できます。

画像のダウンロード

$ curl -o app/assets/images/rails.png -OL railstutorial.jp/rails.png

このコマンドラインを入力するとRailsはimage_tagヘルパーを使っているので、該当する画像ファイルをアセットパイプラインを通してapp/assets/images/ディレクトリの中から探してくれる。

ネコ画像をインターネットからダウンロードする

$ curl -OL cdn.learnenough.com/kitten.jpg

演習

1.は答え書いてあるので省略

2.mvコマンドを使って、ダウンロードしたkitten.jpgファイルを適切なアセットディレクトリに移動してください

mv kitten.jpg app/assets/images/

3.image_tagを使って、kitten.jpg画像を表示してみてください (図 5.4)。

<%= image_tag("kitten.jpg", alt: "cat")%

ちょっと待って。。これだけ言わせて。。。

猫クッソかわええええぇえええええぇぇぇぇ

永遠に愛でていられる、、。
。。。。は!!!!!いかんいかん。
私には時間がないのです。先に進まなくては。。。。

BootstrapとカスタムCSS

次は上記を追加していきます。ここらへん俺progateでやってないんだよな。しっかりまとめないと。
まずBootstrapを追加する。、bootstrap-sass gemを使ってRailsアプリケーションに導入することで使用することができる。
Bootstrapフレームワークでは、動的なスタイルシートを生成するためにLESS CSS言語を使っている。
RailsのAsset Pipelineはデフォルトでは (LESSと非常によく似た) Sass言語をサポートするそのため、bootstrap-sassは、LESSをSassへ変換し、必要なBootstrapファイルを現在のアプリケーションですべて利用できるようにする。

Gemfileにbootstrap-sassを追加する

source 'https://rubygems.org'

gem 'rails',          '5.1.6'
gem 'bootstrap-sass', '3.3.7'
.
.
.

終わったら
$ bundle installを実行。

rails generateコマンドを実行することでコントローラーごとに分けられたCSSファイルが自動的に生成されますが、これらのファイルを正しい順序で読み込ませるのは至難の技らしい。
よってすべてのCSSを1つにまとめる方針を採っています。カスタムCSSを動かすための最初の一歩は、カスタムCSSファイルを作ることである。

$ touch app/assets/stylesheets/custom.scss

app/assets/stylesheets/
Asset Pipelineの一部であり、このディレクトリに置かれたスタイルシートはapplication.cssの一部としてWebサイトのレイアウトに読み込まれます。さらに、ファイル名のcustom.scssには.scssという拡張子も含まれています。この拡張子は「Sass (Sassy CSS)」と呼ばれるCSSを拡張した言語で、アセットパイプラインはこのファイルの拡張子を見て、Sassを処理できるようにしている。

カスタムCSS用のファイルを作成したら、リスト 5.6のように@importを使って、Bootstrap (とそれに関連するSprockets) を読み込みます11。

Bootstrap CSSを追加する

app/assets/stylesheets/custom.scss
---------------------------------
@import "bootstrap-sprockets";
@import "bootstrap";

Bootstrap CSSのフレームワークを導入する。導入後、Webサーバを再起動させると、アプリケーションに反映させることができます。

Ctrl-Cを押してWebサーバを停止させた後、rails server

ここからはCSSを使ってレイアウトの変更が行われる
ここでの演習は確認のみなので省略

パーシャル

レイアウトのコードを整理していく。
HTMLヘッダーは論理的な単位として分けられるため、一箇所にまとめた方が便利です。Railsではパーシャル (partial) という機能でこのような課題を解決できます。

レイアウトにshimとheaderのパーシャルを追加する

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                               'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                               'data-turbolinks-track': 'reload' %>
    <%= render 'layouts/shim' %>
  </head>
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>

次のようにrenderと呼ばれるRailsヘルパー呼び出しだけを使って、HTML shimのスタイルシート行を置換しています。

<%= render 'layouts/shim' %>

この行では、app/views/layouts/_shim.html.erbというファイルを探してその内容を評価し、結果をビューに挿入しています

ファイル名 _shim.html.erb の先頭にあるアンダースコア
このアンダースコアは、パーシャルで使う普遍的な命名規約であり、また、一目見ただけでディレクトリ中のすべてのパーシャルを識別することが可能となる。

パーシャルが動作するためには、それに対応するファイルとコンテンツを記述する。

リスト 5.13: HTML shim用のパーシャル
app/views/layouts/_shim.html.erb
-----------------------------------------------------
<!--[if lt IE 9]>
  <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
  </script>
<![endif]-->

ヘッダーの情報もパーシャルに移動し、renderを呼び出してレイアウトに挿入することができる

リスト 5.14: header用のパーシャル
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", '#', id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home",   '#' %></li>
        <li><%= link_to "Help",   '#' %></li>
        <li><%= link_to "Log in", '#' %></li>
      </ul>
    </nav>
  </div>
</header>

今度はヘッダーに対応するフッタを同じ方法で追加しましょう。_footer.html.erb で、layoutsディレクトリ作成する。

リスト 5.15: footer用のパーシャル
app/views/layouts/_footer.html.erb
<footer class="footer">
  <small>
    The <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    by <a href="http://www.michaelhartl.com/">Michael Hartl</a>
  </small>
  <nav>
    <ul>
      <li><%= link_to "About",   '#' %></li>
      <li><%= link_to "Contact", '#' %></li>
      <li><a href="http://news.railstutorial.org/">News</a></li>
    </ul>
  </nav>
</footer>

ヘッダーの場合と同様に、フッターの中でもlink_toメソッドを使って、AboutページとContactページへの内部リンクを追加する。

リスト 5.16: レイアウトにfooterパーシャルを追加する
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                               'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                               'data-turbolinks-track': 'reload' %>
    <%= render 'layouts/shim' %>
  </head>
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
    </div>
  </body>
</html>

Sassとアセットパイプライン

Rails開発者の視点からは、アセットディレクトリ、マニフェストファイル、プリプロセッサエンジンという、3つの主要な機能が理解の対象となる。

アセットディレクトリ

Railsのアセットパイプラインでは、静的ファイルを目的別に分類する、標準的な3つのディレクトリが使われています。

app/assets: 現在のアプリケーション固有のアセット
lib/assets: あなたの開発チームによって作成されたライブラリ用のアセット
vendor/assets: サードパーティのアセット
これらのディレクトリには、それぞれのアセットクラス用のサブディレクトリがあります。例えばapp/assetsの場合、次のような画像用、JavaScript用、CSS用のサブディレクトリがあります。

$ ls app/assets/
images/  javascripts/  stylesheets/

マニフェストファイル

静的ファイル (アセット) を上記の場所へそれぞれ配置すれば、マニフェストファイルを使って、それらをどのように1つのファイルにまとめるのかをRailsに指示することができます。なお、実際にアセットをまとめる処理を行うのはSprocketsというgemです。また、マニフェストファイルはCSSとJavaScriptには適用されますが、画像ファイルには適用されない。

リスト 5.19: アプリケーション固有のCSS用マニフェストファイル
app/assets/stylesheets/application.css
/*
 * This is a manifest file that'll be compiled into application.css, which
 * will include all the files listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets,
 * vendor/assets/stylesheets, or vendor/assets/stylesheets of plugins, if any,
 * can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear
 * at the bottom of the compiled file so the styles you add here take
 * precedence over styles defined in any styles defined in the other CSS/SCSS
 * files in this directory. It is generally better to create a new file per
 * style scope.
 *
 *= require_tree .
 *= require_self
 */

上の行で重要な部分は、実はCSSコメントの中にあります。コメント内の次の部分は、Sprocketsが適切なファイルを読み込むために使われます。

/*
 .
 .
 .
 *= require_tree .
 *= require_self
*/

例えばこの行は、

 *= require_tree .
app/assets/stylesheetsディレクトリ (サブディレクトリを含む) 中のすべてのCSSファイルが、アプリケーションCSSに含まれるようにしています。

また、次の行は、

 *= require_self
CSSの読み込みシーケンスの中で、application.css自身もその対象に含めています。

Railsには実用的なデフォルトのマニフェストファイルが付属しているので、Railsチュートリアルでは変更を加える必要がありませんが、もし必要な場合は、Railsガイドの「アセットパイプライン」で詳細な情報を参照できます。

プリプロセッサエンジン

必要なアセットをディレクトリに配置してまとめた後、Railsはさまざまなプリプロセッサエンジンを介してそれらを実行し、ブラウザに配信できるようにそれらをマニフェストファイルを用いて結合し、サイトテンプレート用に準備します。Railsはどのプリプロセッサを使うのかを、ファイル名の拡張子を使って判断します。

最も一般的な拡張子は、
Sass用の.scss
CoffeeScript用の.coffee
埋め込みRuby (ERb) 用の.erb

プリプロセッサエンジンは、繋げて実行する (chain) ことができます。

foobar.js.coffee

上の拡張子の場合、CoffeeScriptプロセッサ経由で実行されます。

foobar.js.erb.coffee

上の拡張子の場合は、CoffeeScriptとERbの両方で実行されます (コードは右から左へと実行されますので、この例ではCoffeeScriptが最初に実行される)。

本番環境での効率性

Asset Pipelineがすべてのスタイルシートを1つのCSSファイル (application.css) にまとめ、すべてのJavaScriptファイルを1つのJSファイル (javascripts.js) にまとめてくれる。不要な空白やインデントを取り除く処理を行い、ファイルサイズを最小化してくれる。

素晴らしい構文を備えたスタイルシート

Sassが提供する2つの重要な機能、ネストと変数について説明する。

ネスト

スタイルシート内に共通のパターンがある場合は、要素をネストさせることができます。例えばリスト 5.7では、次のように.centerと.center h1の両方に対してルールがあります。

.center {
  text-align: center;
}

.center h1 {
  margin-bottom: 10px;
}

上のルールは、Sassを使って次のように書き換えることができます。

.center {
  text-align: center;
  h1 {
    margin-bottom: 10px;
  }
}

上の例では、ネストの内側にあるh1というルールは、.centerのルールを継承しています。

#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
}

#logo:hover {
  color: #fff;
  text-decoration: none;
}
上のコードでは#logoというidが2回使われています。1回目はロゴ自身を定義するために、2回目はhover属性を定義するために使われています (なおhover属性は、該当する要素の上にマウスポインタをかざしたときの表示を定義します)。2つ目のルールをネストするためには、親属性である#logoを参照する必要があります。このような場合、SCSSでは次のようにアンパーサンド&を使って実現できます。

#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
  &:hover {
    color: #fff;
    text-decoration: none;
  }
}

Sassは、SCSSをCSSに変換する際に、&:hoverを#logo:hoverに置き換えています。

これらのネスト機能は、フッターのCSSでも使えます。SCSSを使って次のように書き換えることができる。

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid #eaeaea;
  color: #777;
  a {
    color: #555;
    &:hover {
      color: #222;
    }
  }
  small {
    float: left;
  }
  ul {
    float: right;
    list-style: none;
    li {
      float: left;
      margin-left: 15px;
    }
  }
}

変数

Sassでは、冗長なコードを削除し、より自由な表現を可能にするために、変数が定義できるようになっている。

h2 {
  .
  .
  .
  color: #777;
}
.
.
.
footer {
  .
  .
  .
  color: #777;
}

$light-grayのような変数名は、#777のような値よりも分かりやすいので、たとえその変数が繰り返し使われないとしても、変数名を与えることは多くの場合有用です。実際、Bootstrapフレームワークでは、多くの色に対して変数名を定義しています。定義されている変数はBootstrapページの「LESS変数一覧」で参照することができます。

@gray-light: #777;
これはつまり、bootstrap-sassというgemを使えば、SCSSでも同様に$gray-lightという変数が使えることを意味しています。先ほど定義した$light-grayというカスタム変数の代わりに、用意された変数を使ってみましょう。

h2 {
  .
  .
  .
  color: $gray-light;
}
.
.
.
footer {
  .
  .
  .
  color: $gray-light;
}
ネストや変数を使って初期のSCSSファイルを書き直した結果
app/assets/stylesheets/custom.scss
@import "bootstrap-sprockets";
@import "bootstrap";

/* mixins, variables, etc. */

$gray-medium-light: #eaeaea;

/* universal */

body {
  padding-top: 60px;
}

section {
  overflow: auto;
}

textarea {
  resize: vertical;
}

.center {
  text-align: center;
  h1 {
    margin-bottom: 10px;
  }
}

/* typography */

h1, h2, h3, h4, h5, h6 {
  line-height: 1;
}

h1 {
  font-size: 3em;
  letter-spacing: -2px;
  margin-bottom: 30px;
  text-align: center;
}

h2 {
  font-size: 1.2em;
  letter-spacing: -1px;
  margin-bottom: 30px;
  text-align: center;
  font-weight: normal;
  color: $gray-light;
}

p {
  font-size: 1.1em;
  line-height: 1.7em;
}


/* header */

#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: white;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
  &:hover {
    color: white;
    text-decoration: none;
  }
}

/* footer */

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid $gray-medium-light;
  color: $gray-light;
  a {
    color: $gray;
    &:hover {
      color: $gray-darker;
    }
  }
  small {
    float: left;
  }
  ul {
    float: right;
    list-style: none;
    li {
      float: left;
      margin-left: 15px;
    }
  }
}

レイアウトのリンク

次にレイアウトのリンクを書き換えていく。
'#'で代用していたリンクを書き換えてみる。
もちろん、次のようにリンクを直接記述することもできる。

<a href="/static_pages/about">About</a>

しかし、上の記法はRails流ではありません。まず、aboutページへのURLは /static_pages/about よりも /about の方がよいでしょう。さらに、Railsでは次のようなコードでは名前付きルートを使うのが慣例となっています。

<%= link_to "About", about_path %>

上のようにすることでコードの意味がわかりやすくなり、about_pathの定義を変えればabout_pathが使われているすべてのURLを変更できるため、柔軟性が高まる。

今後使う予定のURLとルーティング (route) とのマッピングを、表に示す。

ページ名    URL      名前付きルート
Home     /           root_path
About    /about      about_path
Help     /help        help_path
Contact  /contact    contact_path
Sign up  /signup     signup_path
Log in   /login      login_path

RailsのルートURL

名前付きルートをサンプルアプリケーションの静的ページで使うために、ルーティング用のファイル (config/routes.rb) を編集していく。

ルートURLを定義すると、root_pathやroot_urlといったメソッドを通してURLを参照することができます。ちなみに前者はルートURL以下の文字列を、後者は完全なURLの文字列を返します。

root_path -> '/'
root_url  -> 'http://www.example.com/'

なお、Railsチュートリアルでは一般的な規約に従い、基本的には_path書式を使い、リダイレクトの場合のみ_url書式を使うようにします。これはHTTPの標準としては、リダイレクトのときに完全なURLが要求されるためです。ただしほとんどのブラウザでは、どちらの方法でも動作します。
デフォルトのルーティングはやや回りくどいので、HelpページやAboutページ、Contactページなどの名前付きルートを定義していきましょう。具体的には、getルールを使って定義していきます

例えば次のようなルーティングは、

get 'static_pages/help'

このように変換します。

get  '/help', to: 'static_pages#help'

このようにgetルールを使って変更すると、GETリクエストが /help に送信されたときにStaticPagesコントローラーのhelpアクションを呼び出してくれるようになります。また、ルートURLのときと同様に、help_pathやhelp_urlといった名前付きルートも使えるようになります。

help_path -> '/help'
help_url  -> 'http://www.example.com/help'

他の静的ページについても同様にルーティングを変更していくと、リスト 5.23はリスト 5.27のようなコードになります。

リスト 5.27: 静的なページのルーティング一覧 red

config/routes.rb

Rails.application.routes.draw do
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
end

'static_pages/home'という以前のルールを削除している点に注意する。今後は常にroot_pathまたはroot_urlを使っていきます。

リスト 5.28: StaticPagesで扱う新しい名前付きルートに対するテスト green
test/controllers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get root_path
    assert_response :success
    assert_select "title", "Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get help_path
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get about_path
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end

  test "should get contact" do
    get contact_path
    assert_response :success
    assert_select "title", "Contact | Ruby on Rails Tutorial Sample App"
  end
end

名前付きルート

リスト 5.27でルートを定義したことにより、レイアウトの中で名前付きルートが使えるようになりました。早速、link_toメソッドの2番目の引数で、適切な名前付きルートを使ってみる。

<%= link_to "About", '#' %>

このように置き換えます。

<%= link_to "About", about_path %>

最初に、HomeページとHelpページへのリンクを持つheaderパーシャル _header.html.erb (リスト 5.30) から取り掛かります。headerパーシャルでは、Web共通の慣習に従って、ロゴにもHomeページへのリンクを追加します。

リスト 5.30: headerパーシャルにリンクを追加する

app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home",    root_path %></li>
        <li><%= link_to "Help",    help_path %></li>
        <li><%= link_to "Log in", '#' %></li>
      </ul>
    </nav>
  </div>
</header>

[Log in] リンクの名前付きルートは第8章で作成するため、今の段階では'#'のままにしておきます。

footerパーシャル _footer.html.erb にもリンクがあります。これらはAboutページとContactページへのリンクです (リスト 5.31)。

リスト 5.31: footerパーシャルにリンクを追加する
app/views/layouts/_footer.html.erb
<footer class="footer">
  <small>
    The <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    by <a href="http://www.michaelhartl.com/">Michael Hartl</a>
  </small>
  <nav>
    <ul>
      <li><%= link_to "About",   about_path %></li>
      <li><%= link_to "Contact", contact_path %></li>
      <li><a href="http://news.railstutorial.org/">News</a></li>
    </ul>
  </nav>
</footer>

5.3.4 リンクのテスト
レイアウト内のいくつかのリンクを埋めることができたので、これらのリンクが正しく動いているかどうかチェックするテストを書いてみましょう。

まず統合テストの作成

$ rails generate integration_test site_layout
      invoke  test_unit
      create    test/integration/site_layout_test.rb

このとき、Railsは渡されたファイル名の末尾に _test という文字列を追加することに注目してください。

今回の目的は、アプリケーションのHTML構造を調べて、レイアウトの各リンクが正しく動くかどうかチェックすることです。つまり、

ルートURL (Homeページ) にGETリクエストを送る.
正しいページテンプレートが描画されているかどうか確かめる.
Home、Help、About、Contactの各ページへのリンクが正しく動くか確かめる.
Railsの統合テストでは、上のステップをコードに落とし込んでいくことになります (リスト 5.32)。具体的には、まずassert_templateメソッドを使って、Homeページが正しいビューを描画しているかどうか確かめます16。

リスト 5.32: レイアウトのリンクに対するテスト green
test/integration/site_layout_test.rb
require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest

  test "layout links" do
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
  end
end

assert_selectメソッドの高度なオプションを使っていく。
今回のケースでは、特定のリンクが存在するかどうかを、aタグとhref属性をオプションで指定して調べていく。

assert_select "a[href=?]", about_path
上のコードでは、Railsは自動的にはてなマーク "?" をabout_pathに置換しています (このとき "about_path" 内に特殊記号があればエスケープ処理されます)。これにより、次のようなHTMLがあるかどうかをチェックすることができます。

...
一方で、ルートURLへのリンクは2つあることを思い出してください (1つはロゴに、もう1つはナビゲーションバーにあります)。このようなとき、

assert_select "a[href=?]", root_path, count: 2
といった風に書くことで、リスト 5.30で定義したHomeページのリンクの個数も調べることもできます

assert_selectには色々な指定の仕方があります。その代表例をいくつか表 5.2で紹介します。assert_selectは柔軟でパワフルな機能で、ここでは紹介し切れないほど他にも多くのオプションがあります。しかし経験的には、このメソッドで複雑なテストはしない方が賢明です。今回のようなレイアウト内で頻繁に変更されるHTML要素 (リンクなど) をテストするぐらいに抑えておくとよいです。

Code    マッチするHTML
assert_select "div" <div>foobar</div>
assert_select "div", "foobar"   <div>foobar</div>
assert_select "div.nav" <div class="nav">foobar</div>
assert_select "div#profile" <div id="profile">foobar</div>
assert_select "div[name=yo]"    <div name="yo">hey</div>
assert_select "a[href=?]", '/', count: 1    <a href="/">foo</a>
assert_select "a[href=?]", '/', text: "foo" <a href="/">foo</a>

assert_selectのいくつかの使用例
リスト 5.32で追加した統合テストが通るかどうかは、次のようにRakeタスクを実行することで試すことができます。

リスト 5.33: green
$ rails test:integration
統合テストが成功したら、今度はすべてのテストを流して greenするかどうか確かめてみてください。

リスト 5.34: green
$ rails test
レイアウトのリンクをテストする統合テストが追加されたことで、リンクに間違った変更が加えられたらすぐに気付けるようになりました。

class ActiveSupport::TestCase
  fixtures :all
  include ApplicationHelper
  .
  .
  .
end

ユーザー登録:最初のステップ

ユーザー登録ページへのルーティングを作成する。

Usersコントローラ

最初のコントローラであるStaticPagesコントローラを作成しました。今度は2番目のコントローラであるUsersコントローラを作成しましょう。以前のときと同様に、generateを実行して、現時点での要求である新規ユーザー用のユーザー登録ページ (スタブ) を持つ、最も簡単なコントローラを作成します。Railsで好まれているRESTアーキテクチャの規約に従い、新規ユーザー用のアクションをnewとする。したがって、generate controllerの引数にnewを渡して、自動的にアクションを作成する。

リスト 5.38: Usersコントローラの生成 (newアクションを追加)
$ rails generate controller Users new
      create  app/controllers/users_controller.rb
       route  get 'users/new'
      invoke  erb
      create    app/views/users
      create    app/views/users/new.html.erb
      invoke  test_unit
      create    test/controllers/users_controller_test.rb
      invoke  helper
      create    app/helpers/users_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/users.coffee
      invoke    scss
      create      app/assets/stylesheets/users.scss

newアクションを持つUsersコントローラと、スタブのユーザービューを作成します。このとき、新しいUserページ用の小さなテストも生成されていて、この時点ではパスするはずです。

newアクションを持つ最初のUsersコントローラ

app/controllers/users_controller.rb
class UsersController < ApplicationController

  def new
  end
end
リスト 5.40: Users用の最初のnewアクション
app/views/users/new.html.erb
<h1>Users#new</h1>
<p>Find me in app/views/users/new.html.erb</p>
リスト 5.41: Userページ用の最初のテスト green
test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
    get users_new_url
    assert_response :success
  end
end

この時点では、テストは greenになっているはずです。

演習

1.users_new_urlではなくsignup_pathを使えるようにしてみてください。

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  test "should get new" do
    get singup_path
    assert_response :success
  end
end

greenになるよう修正します。
ユーザー登録用URL
コードにより、新規ユーザー用の動作するページが/users/new にできました。ここで表 5.1を思い出していただきたいのですが、URLは/users/newではなく表のとおりに/signupにしたいと思います。例に従い、ユーザー登録URL用にget '/signup'のルートを追加します.

ユーザー登録ページのルート red
config/routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
  get  '/signup',  to: 'users#new'
end
リスト 5.44: Usersコントローラのテストで名前付きルートを使うようにする green
test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
    get signup_path
    assert_response :success
  end
end

次に、新しく定義された名前付きルートを使って、Homeページのボタンに適切なリンクを追加します。他のルートと同様、get ’/signup’と記述したことでsignup_pathという名前付きルートができ、それをリスト 5.45で使います。なお、signupページへのテストは演習に回すことにします (5.3.2.1)。

リスト 5.45: ボタンにユーザー登録ページへのリンクを追加する
app/views/static_pages/home.html.erb
<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>

  <h2>
    This is the home page for the
    <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    sample application.
  </h2>

  <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.png", alt: "Rails logo"),
            'http://rubyonrails.org/' %>
最初のユーザー登録ページ (スタブ)
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<p>This will be a signup page for new users.</p>

とりあえず足早に行きたかったので今日はここらへんにしときます。

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

Rails Backend で Sign in with Apple の認証処理を実装する

はじめに

とある理由により Sign in with Apple (以下、SIWA) の実装をしなければならなくなったのですが、Appleの公式ドキュメント を読んでいても、 iOSアプリ + Railsバックエンド という構成のサービスが、一体どのようにして SIWA 機能を実装するかなどの手順や情報が乏しすぎて非常に理解に苦しんだので、OpenID Connect などの知識と組み合わせながら、実装した結果とSIWAで調べたことなどをここに残します。

前提

こちらの記事は、OAuth2認証OpenID Connect の知識を前提に書いております。
僕は、以下の記事が参考になりました。
https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f
https://qiita.com/TakahikoKawasaki/items/498ca08bbfcc341691fe

アプリの実装に関しては一切触れていません。あくまでサーバ側のみです。

各種Appleでの設定に関してはこことかが参考になりそうです。
https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003

実装例

https://github.com/y4m4p/siwa_rails

認証処理の流れと全体像

  1. ユーザが アプリから AppleID での認証を開始する
  2. アプリが Appleから authorization_codeid_token を入手する
  3. アプリが 2で手に入れた情報を サーバ側に渡す
  4. サーバが authorization_codeを Appleの認可サーバに渡す
  5. Appleの認可サーバが authorization_codeと引き換えにid_tokenなどの情報を サーバに返す
  6. サーバが 3で手に入れた id_token 5で手に入れたid_token 検証する
  7. サーバが リソースへのアクセスを ユーザに付与する (※こちらは各種サービスの実装依存なので割愛)

Untitled Diagram-Page-1.jpg

シーケンス図で描くと以下のようになります
bPEzRkim38LtFmNXvTuxv0KoN2H0bonjWQ1RW82Hc5g8AmaKxRHz-kh7AfIWImTBiCbppaV1sWH1bkbjx0QZ2SxEoSczTnqgSGmlWHWmm1JtpiT6eun7Az0_efYPjjuVx5_pjF_NfpRq_hPh-xo3FV5C7DiOpHXM9rGNihe8GgxTQRmVgTgDb57CZ4A58Ysm-f-AQp3MJm9PiNi0j1eyXd1rh1jPeehF8Xff3Tj91h8nihh2.png

SIWAの細かい話

認証処理の実装について

上記の 1と2 の処理において Apple Server は [authorization_code および id_token] を アプリに返却しているため、OpenID Connect Section 3. を根拠に Sign in with Apple は "Hybrid Flow" を採用していることが導き出せる。

Hybrid Flow の場合、
https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowSteps
に記載されている方法でユーザの情報を取得しなければならないことが明記されている。
(以下では、SIWA仕様に読み替えて、手順を簡略化する)

  1. 「クライアント」が、Appleの認可エンドポイントにエンドユーザの認証を行うためのリクエストを送る。
  2. Apple の認可エンドポイント から「クライアント」に authorization_code とレスポンスタイプによって1つ以上の付加パラメータを送る。SIWA場合、付加パラメータが id_tokenname である。
  3. 「クライアント」が、authorization_code を利用して Apple の トークンエンドポイントにリクエストを送り、id_tokenaccess_token が含まれるレスポンスをもらう。
  4. 「クライアント」は トークンエンドポイントから返ってきた id_token の検証を行い エンドユーザの識別情報を得る。

ここで、「クライアント」(client)と言っているのは 「アプリ + サーバ」 であることに留意したい。
そのため、「誰が」認証エンドポイントにリクエストを投げ、「誰が」トークンエンドポイントにリクエストを投げて検証するかは、完全に実装する側に委ねられている。
OAuth 2.0 RFC 6749 の clientの定義より

client
An application making protected resource requests on behalf of the
resource owner and with its authorization. The term "client" does
not imply any particular implementation characteristics (e.g.,
whether the application executes on a server, a desktop, or other
devices).

1と2 (ユーザ認証処理) をアプリが行い、3と4 (認証情報の検証)に関しては サーバが行うような実装とするのが適切であろう。

サーバ側で行わなければならない検証項目は、OpenID Connect Section 3.3.3.5. を根拠に特に以下の2つである。

A. authorization_code の検証
B. id_token の 検証

以下に、それぞれについて詳しく書く。

A. authorization_code の検証について

検証方法
検証内容
  • アプリからサーバに送られてきた authorization_codeid_token とペアであることを検証する。
    • この検証に失敗する場合、第三者が不正に取得した authorization code あるいは、id_token を忍び込ませている可能性がある

B. ID Token の検証について

検証方法
検証内容について
  • 発行者(iss)情報が、両方のid_token で一致していること
  • ユーザの一意な識別子(sub)が、両方のid_token で一致していること
  • nonce 値が、両方のid_token で一致していること
    • All Claims about the Authentication event present in either SHOULD be present in both. の解釈より
  • email などのユーザ情報に関する値が 両方のid_token で存在する場合に一致していること
    • If either ID Token contains Claims about the End-User, any that are present in both SHOULD have the same values in both. の解釈より
  • id_token が失効していないこと
    • 失効しているトークンを受けないようにするため
  • id_token の jwt の署名を確認すること
    • Appleの秘密鍵によって、署名が行われていることを確かめることで発行者情報を強固にする
    • 逆に署名が誤っている場合は、元のトークン情報が改ざんされている可能性がある

SIWA と 一般的な OpenID Connect 準拠の認証方法との違いについて

丁寧にも、OpenID Connect財団の方たちが、「SIWA が 一般的なOpenID Connectの仕様と、どういう点で異なるのか」を Peculiarities の欄で全て列挙してくれている。
https://bitbucket.org/openid/connect/src/default/How-Sign-in-with-Apple-differs-from-OpenID-Connect.md
以下、Peculiarities 欄を一部訳すると以下のような事が書かれている。

  • Discovery document 用のエンドポイントが提供されていないので、開発者がエンドポイントやスコープ、署名アルゴリズムなどの情報を得たいと思ったときに、毎度Appleのドキュメントを精読する必要があること。本来は Discovery document を用意するのが推奨されている。

  • UserInfo エンドポイントが提供されていないので、ユーザに関する情報は全て(失効する・潜在的に大きなデータになり得る)id_token に含まれる事になっていること。

  • アプリケーションからの scope の指定は、「最も最初のリクエスト(すなわち連携時)」のときのみ有効になっている。もし、連携時のときに 氏名(name)だけの scope を指定してユーザが許可した場合、そのユーザの email は二度とアプリケーション側から取得しに行くことは出来ない。
    (アプリケーションからユーザ情報の取得を行う処理の不自由さを指している)

... など、かなり独自の実装が行われている事が伺える。

公式ドキュメント

Apple

OpenID Connect

OAuth 2.0

その他の参考資料

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

ショッピングサイトアプリの購入機能(model)をRSpecとFactory�botで登録テストの構築をしてみた。

はじめに

ショッピングサイトアプリ(疑似フリマアプリ)を作成していて、購入機能を担当することになりました。
「RSpecでmodelのテストコードを作成せよ」とのお達しがありましたので、取り組んでみました。

その結果、associationで「belongs_to」の関係にあるモデルの連携に
ハマりましたので、備忘録もかねて投稿します。

前提

ここでは支払処理が完了した後に購入記録を登録する機能のテストを説明しています。
支払機能(payjplなどのAPI)について触れませんのでご了承ください。

参考記事

【FactoryBot】associationの使い方

開発環境

ruby 2.5.1
rails 5.2.4.1
RSpec 3.9.0
factory_bot 5.1.1
devise 4.7.1

ER図

ショッピングサイトアプリのテーブル構成を次のように定義するとします。

スクリーンショット 2020-03-25 22.19.32.png

一見するとわかりにくいかもしれませんが、
このような考え方になります。

  1. 出品者

    • ユーザー登録をする。(user)
    • 発送元住所の登録をする。(address)
    • 売りたいモノを出品する。 (item)
  2. 購入者

    • ユーザー登録をする。(user)
    • 発送先住所の登録をする。(address)
    • 出品されたモノを購入する。 (trade)

ここで注意しなければならないことは

  • userとaddressは出品者と購入者で必要であること。
  • itemは出品者のみでよいこと。
  • tredeは出品者となるitemと、購入者であるuserとaddressが必要であること。

となります。

他の開発者が作成したfactoryデータコードを流用しようとしましたが、出品者と購入者の2種類を同時に
テストするように想定されていないため、使い回しができませんでした(横着をしてはいけない)。

よって、購入機能専用に出品者と購入者のコードを書くことにしました。

Factorybotデータを作ろう

まずはuserから

spec/factories/users.rb
FactoryBot.define do
  # 出品者用データ factory名は「seller」とします。
  # 「sequence」はspec.rbから呼び出されるたびにカウントアップしてくれる機能です。
  factory :seller, class: User do
    sequence(:nickname)        { |i| "出品者_#{i}"}
    sequence(:email)           { |i| "seller_#{i}@test.com"}
    password                   {"00000000b"}
    last_name                  {"苗字"}
    first_name                 {"名前"}
    last_name_kana             {"ミョウジカナ"}
    first_name_kana            {"ナマエカナ"}
    birthday                   {"20190101"}
    telephone_number           {"1234567890"}
  end

  # 購入者用データ factory名は「buyer」とします。
  factory :buyer, class: User do
    sequence(:nickname)        { |i| "購入者_#{i}"}
    sequence(:email)           { |i| "byuer_#{i}@test.com"}
    password                   {"00000000c"}
    last_name                  {"苗字"}
    first_name                 {"名前"}
    last_name_kana             {"ミョウジ"}
    first_name_kana            {"ナマエ"}
    birthday                   {"20190101"}
    telephone_number           {"1234567890"}
  end
end

続いてarea

spec/factories/areas.rb
FactoryBot.define do
  # 出品者地域 
  factory :seller_area, class: Area do
    name {"北海道"}
  end

  # 購入者地域 
  factory :buyer_area, class: Area do
    name {"東京都"}
  end
end

さらにaddresses

spec/factories/addrsses.rb
FactoryBot.define do
  # 出品者住所
  factory :seller_address, class: Address do

    zip_code         {"1234567"}
    city             {"city_1"}
    number           {"number_1"}
    building         {"building_1"}
    last_name        {"出品"}
    first_name       {"太郎"}
    telephone_number {"03-1234-5678"}

    #出品者のuserとaddressと連携します。
    association :area, factory: :seller_area
    association :user, factory: :seller

  end

  # 購入者住所
  factory :buyer_address, class: Address do

    zip_code         {"3214567"}
    city             {"city_2"}
    number           {"number_2"}
    building         {"building_2"}
    last_name        {"購入"}
    first_name       {"次郎"}
    last_name_kana   {"ジロウ"}
    first_name_kana  {"コウニュウ"}
    telephone_number {"03-1234-5678"}

    #購入者のuserとaddressと連携します。
    association :area, factory: :buyer_area
    association :user, factory: :buyer

  end
end

やっとitems

spec/factories/items.rb
FactoryBot.define do

  # 出品者用データ
  factory :seller_item, class: Item do

    sequence(:title) { |i| "product_#{i}"}
    sequence(:description) { |i| "description_#{i}"}

    price            {1000.000}

    #出品者のaddressと連携します。
    association :address, factory: :selladdress
    #userはaddress内にあるuserと連携します。
    user             {address.user}

  end
end

最後にtrades

spec/factories/trades.rb
FactoryBot.define do
  #購入用データ
  factory :trade do
    status_num  {0}

    #出品者のitemと連携します。
    association :item, factory: :seller_item

    #購入者のaddressと連携します。
    association :address, factory: :buyer_address
    #購入者のuserは購入者のaddress内にあるuserと連携します。
    user        {address.user}
  end

end

RSpecのテストコードを作ろう

tradeのテストコード
たったこれだけ

spec/models/trade_spec.rb
require 'rails_helper'

RSpec.describe Trade, type: :model do
  describe "#create" do
    #associationで関連付けした場合は、buildではなくcreateを使います。
    let(:trade)    { create(:trade) }
    it "is valid trade" do
      #ここでtradeを呼び出すことで、「let(:trade)」が実行されます。
      expect(trade).to be_valid
    end
  end
end

ターミナルでテストを実行すると

こうなりました。

ターミナル
% bundle exec rspec spec/models/trade_spec.rb

Trade
  #create
    is valid trade

Finished in 1.12 seconds (files took 5.19 seconds to load)
1 examples, 0 failures
% 

「is valid trade」と表示されたので、
登録テストが正常終了したことがわかりました。

まとめ

  • 購入機能テスト用のfactoryデータは、出品者と購入者それぞれに適用するように作成する必要があること。
  • 他の開発者が作成したfactoryコードは流用ができないこと。(そもそも購入機能を想定して作られたモノではなかったため)

となりました。

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

ショッピングサイトアプリの購入機能テスト(model)をRSpecとFactory�botで構築してみた。

はじめに

ショッピングサイトアプリ(疑似フリマアプリ)を作成していて、購入機能を担当することになりました。
「RSpecでmodelのテストコードを作成せよ」とのお達しがありましたので、取り組んでみました。

その結果、associationで「belongs_to」の関係にあるモデルの連携に
ハマりましたので、備忘録もかねて投稿します。

前提

ここでは支払処理が完了した後に購入記録を登録する機能のテストを説明しています。
支払機能(payjplなどのAPI)について触れませんのでご了承ください。

参考記事

【FactoryBot】associationの使い方

開発環境

ruby 2.5.1
rails 5.2.4.1
RSpec 3.9.0
factory_bot 5.1.1
devise 4.7.1

ER図

ショッピングサイトアプリのテーブル構成を次のように定義し、
rails db:migrateコマンドじ実行とmodelの構築が済んだものとします。

スクリーンショット 2020-03-25 22.19.32.png

一見するとわかりにくいかもしれませんが、
このような考え方になります。

  1. 出品者

    • ユーザーを登録する。(user)
    • 発送元住所を登録する。(address)
    • 売りたいモノを出品する。 (item)
  2. 購入者

    • ユーザーを登録する。(user)
    • 発送先住所を登録する。(address)
    • 出品されたモノを購入する。 (trade)

ここで注意しなければならないことは

  • userとaddressは出品者と購入者それぞれに必要であること。
  • itemは出品者のみでがuserとaddressに従属すること。
  • tredeは出品者のitemと、購入者のuserとaddressが必要であること。

となります。

他の開発者が作成したfactoryデータコードを流用しようとしましたが、出品者と購入者の2種類を同時に
テストするように想定されていないため、使い回しができませんでした(横着をしてはいけない)。

よって、購入機能専用に出品者と購入者のコードを書くことにしました。

Factorybotでテストデータを作ろう

まずはusers

spec/factories/users.rb
FactoryBot.define do
  # 出品者用データ factory名は「seller」とします。
  # 「sequence」はspec.rbから呼び出されるたびにカウントアップしてくれる機能です。
  factory :seller, class: User do
    sequence(:nickname)        { |i| "出品者_#{i}"}
    sequence(:email)           { |i| "seller_#{i}@test.com"}
    password                   {"00000000b"}
    last_name                  {"苗字"}
    first_name                 {"名前"}
    last_name_kana             {"ミョウジカナ"}
    first_name_kana            {"ナマエカナ"}
    birthday                   {"20190101"}
    telephone_number           {"1234567890"}
  end

  # 購入者用データ factory名は「buyer」とします。
  factory :buyer, class: User do
    sequence(:nickname)        { |i| "購入者_#{i}"}
    sequence(:email)           { |i| "byuer_#{i}@test.com"}
    password                   {"00000000c"}
    last_name                  {"苗字"}
    first_name                 {"名前"}
    last_name_kana             {"ミョウジ"}
    first_name_kana            {"ナマエ"}
    birthday                   {"20190101"}
    telephone_number           {"1234567890"}
  end
end

続いてareas

spec/factories/areas.rb
FactoryBot.define do
  # 出品者地域 
  factory :seller_area, class: Area do
    name {"北海道"}
  end

  # 購入者地域 
  factory :buyer_area, class: Area do
    name {"東京都"}
  end
end

さらにaddresses

spec/factories/addrsses.rb
FactoryBot.define do
  # 出品者住所
  factory :seller_address, class: Address do

    zip_code         {"1234567"}
    city             {"city_1"}
    number           {"number_1"}
    building         {"building_1"}
    last_name        {"出品"}
    first_name       {"太郎"}
    telephone_number {"03-1234-5678"}

    #出品者のareaとuserを連携します。
    association :area, factory: :seller_area
    association :user, factory: :seller

  end

  # 購入者住所
  factory :buyer_address, class: Address do

    zip_code         {"3214567"}
    city             {"city_2"}
    number           {"number_2"}
    building         {"building_2"}
    last_name        {"購入"}
    first_name       {"次郎"}
    last_name_kana   {"ジロウ"}
    first_name_kana  {"コウニュウ"}
    telephone_number {"03-1234-5678"}

    #購入者のareaとuserを連携します。
    association :area, factory: :buyer_area
    association :user, factory: :buyer

  end
end

やっとitems

spec/factories/items.rb
FactoryBot.define do

  # 出品者用データ
  factory :seller_item, class: Item do

    sequence(:title) { |i| "product_#{i}"}
    sequence(:description) { |i| "description_#{i}"}

    price            {1000.000}

    #出品者のaddressと連携します。
    association :address, factory: :selladdress
    #出品者のaddress内にあるuserと連携します。
    user             {address.user}

  end
end

最後にtrades

spec/factories/trades.rb
FactoryBot.define do
  #購入用データ
  factory :trade do
    status_num  {0}

    #出品者のitemと連携します。
    association :item, factory: :seller_item

    #購入者のaddressと連携します。
    association :address, factory: :buyer_address
    #購入者のaddress内にあるuserと連携します。
    user        {address.user}
  end

end

RSpecでテストコードを作ろう

tradeのテストコードはたったこれだけ

spec/models/trade_spec.rb
require 'rails_helper'

RSpec.describe Trade, type: :model do
  describe "#create" do
    #factoryデータを呼び出すときに、associationで関連付けした場合は、createを使います。
    #buildを使うとテストそのものが失敗します。
    let(:trade)    { create(:trade) }
    it "is valid trade" do
      #ここでtradeを呼び出すことで、「let(:trade)」が実行されます。
      expect(trade).to be_valid
    end
  end
end

ターミナルでテストを実行すると

こうなりました。

terminal
% bundle exec rspec spec/models/trade_spec.rb

Trade
  #create
    is valid trade

Finished in 1.12 seconds (files took 5.19 seconds to load)
1 examples, 0 failures
% 

「is valid trade」と表示されたので、
登録テストが正常終了したことがわかりました。

まとめ

  • 購入機能テスト用のfactoryデータは、出品者と購入者それぞれに必要な範囲で作成しなければならないこと。
  • 他の開発者が作成したfactoryコードは流用しない方が良いこと。(そもそも購入機能を想定して作られたモノではないことがほとんど)

となりました。

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

【Ruby on Rails】Webpackerでcocoonを導入方法

cocoonを使うなら、jQueryが必要なので、WebpackerでjQueryを導入方法はこちらです:
【Ruby on Rails】WebpackerでBootstrap、jQueryを導入方法

Gemfile
gem "cocoon"

以下のファイルをダウンロードして、app/javascript/src/に保存する。
https://github.com/nathanvda/cocoon/blob/master/app/assets/javascripts/cocoon.js

app/javascript/packs/application.js
require("../src/cocoon");
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]active_hash

はじめに

現在個人で作成しているアプリで都道府県のデータをテーブルに持たせて使用していたのですが、調べたところ
どうやらactive_hashなる便利なものがあると知ったため、実際にその導入するまでを書きたいと思います。

active_hash

gemをインストールすることで使用ができます。
モデルにハッシュでデータを持たせることで、そのデータをActiveRecordと同じような感覚で使えます。
つまり都道府県など静的データを扱う際、わざわざテーブルを作成する必要がなくなります。
便利ですね。

Gemインストール

Gemfileに以下を記載して,bundle installをします。

Gemfile.
gem 'active_hash'

モデル作成

今回はLetterモデルとPrefectureモデルを作成し、Prefectureモデルに都道府県データを持たせます。
letterモデルには都道府県のデータを保存するカラムprefecture_idを作成します。

rails g model letter prefecture_id:integer  #letterモデル作成

rails db:migrate  #letterテーブルの作成

これでletterモデルは作成できました。
次にPrefectureモデルですが、これはテーブルを持たないためrails g modelで作成するのではなく、自分で作成します。
なおその際、ActiveHash::Baseを継承させます。
モデルを作成したら使用する都道府県のデータをハッシュの形で記述します。
すると以下のようになります

app/models/prefecture.rb
class Prefecture < ActiveHash::Base
  self.data = [
      {id: 1, name: '北海道'}, {id: 2, name: '青森県'}, {id: 3, name: '岩手県'},
      {id: 4, name: '宮城県'}, {id: 5, name: '秋田県'}, {id: 6, name: '山形県'},
      {id: 7, name: '福島県'}, {id: 8, name: '茨城県'}, {id: 9, name: '栃木県'},
      {id: 10, name: '群馬県'}, {id: 11, name: '埼玉県'}, {id: 12, name: '千葉県'},
      {id: 13, name: '東京都'}, {id: 14, name: '神奈川県'}, {id: 15, name: '新潟県'},
      {id: 16, name: '富山県'}, {id: 17, name: '石川県'}, {id: 18, name: '福井県'},
      {id: 19, name: '山梨県'}, {id: 20, name: '長野県'}, {id: 21, name: '岐阜県'},
      {id: 22, name: '静岡県'}, {id: 23, name: '愛知県'}, {id: 24, name: '三重県'},
      {id: 25, name: '滋賀県'}, {id: 26, name: '京都府'}, {id: 27, name: '大阪府'},
      {id: 28, name: '兵庫県'}, {id: 29, name: '奈良県'}, {id: 30, name: '和歌山県'},
      {id: 31, name: '鳥取県'}, {id: 32, name: '島根県'}, {id: 33, name: '岡山県'},
      {id: 34, name: '広島県'}, {id: 35, name: '山口県'}, {id: 36, name: '徳島県'},
      {id: 37, name: '香川県'}, {id: 38, name: '愛媛県'}, {id: 39, name: '高知県'},
      {id: 40, name: '福岡県'}, {id: 41, name: '佐賀県'}, {id: 42, name: '長崎県'},
      {id: 43, name: '熊本県'}, {id: 44, name: '大分県'}, {id: 45, name: '宮崎県'},
      {id: 46, name: '鹿児島県'}, {id: 47, name: '沖縄県'}
  ]
end

アソシエーションの定義

prefecture.rbに記述は必要ないのですがletter.rbにのみアソシエーションの定義をします。その際active_hashのbelongs_to_active_hashメソッドを使用して定義をします。そして、active_hashを使用するためextend ActiveHash::Associations::ActiveRecordExtensionsも追記します。すると以下のようになります。

app/models/letter.rb
class Letter < ApplicationRecord
  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to_active_hash :prefecture
end

これで使用できるようになります。
Prefectureモデルのテーブルはありませんがテーブルのデータを扱う感覚で以下のように書くことで使用することもできます。

active_hash.html.haml
= f.collection_select :prefecture_id, Prefecture.all, :id, :name

おわり

静的データのためにテーブルをもつ必要がなく、かつハッシュに格納したデータをテーブルのデータのように扱うこともできるという利点はとても魅力的ですよね。
最後まで読んでいただきありがとうございました。

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

【Ruby on Rails】WebpackerでFontAwesomeを導入方法

Gemのインストール

必須ではないですが、Viewでiconメソッドを使うならば、Gemをインストールする必要です。

Gemfile
gem "font-awesome-sass"

FontAwesomeの導入

yarn add @fortawesome/fontawesome-free
app/javascript/packs/application.js
import "@fortawesome/fontawesome-free/js/all"
app/javascripts/src/application.scss
@import "~@fortawesome/fontawesome-free/scss/fontawesome";

>>> FontAwesome free icons

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

【Ruby on Rails】WebpackerでBootstrap、jQueryを導入方法

Webpackerのインストール

Gemfile
gem "webpacker"

bundle install

rails webpacker:install

Bootstrapの導入

yarn add bootstrap@4.3.1 jquery popper.js
webpacker/environment.js
const { environment } = require('@rails/webpacker')

// ↓↓↓
const webpack = require('webpack')
environment.plugins.append(
    'Provide',
    new webpack.ProvidePlugin({
        $: 'jquery',
        jQuery: 'jquery',
        Popper: ['popper.js', 'default']
    })
)
// ↑↑↑

module.exports = environment
app/javascript/packs/application.js
import "bootstrap"
import "../src/application.scss"

require("jquery")
app/javascript/src/application.scss
@import "~bootstrap/scss/bootstrap";
app/views/layouts/application.html.slim
html
  head
    == javascript_pack_tag "application"
    == stylesheet_pack_tag "application"

Webpacker導入後、data-remoteやdata-confirmが動かなくなった場合

yarn add rails-ujs
app/javascript/packs/application.js
import Rails from "rails-ujs";
Rails.start();

Webpacker導入後、*.js.erbファイルが動かなくなった場合

app/javascript/packs/application.js
import $ from "jquery";
global.$ = jQuery;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gem rack-cors の README を翻訳しました

概要

gem rack-corsREADME を翻訳しました。

Rack CORS Middleware

Rack::Cors は Rack と互換性のあるウェブアプリケーションのために Cross-Origin Resource Sharing (CORS) のサポートを提供します。

CORS を使うことで、Webアプリケーションは JSONP などの回避策を使用せずにクロスドメインの AJAX の呼び出しを行うことができます。Cross-domain Ajax with Cross-Origin Resource Sharing をご覧ください。

インストール

gem をインストールしてください。

gem install rack-cors

あるいは Gemfile に以下を記載してください。

gem 'rack-cors'

設定

Rails の設定

Rails アプリケーションの config/application.rb に下記のようなコードを記載します。たとえば、これによってすべてのオリジンからのすべてのリソースに対する GET, POST, OPTIONS リクエストが許可されます。

module YourApp
  class Application < Rails::Application
    # ...

    # Rails 5

    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*', headers: :any, methods: [:get, :post, :options]
      end
    end

    # Rails 3/4

    config.middleware.insert_before 0, "Rack::Cors" do
      allow do
        origins '*'
        resource '*', headers: :any, methods: [:get, :post, :options]
      end
    end
  end
end

insert_before を使用して、Rack::Cors がスタックの先頭で実行され、他のミドルウェアによる干渉を受けないようにします(共通の落とし穴 セクションの Rack::Cache の記載をご覧ください)。

rack ミドルウェアの詳細を知るために Rails Guide to Rack あるいは railscast をご覧ください。

Rack 設定

注意: もし Rails を起動しているなら、config/application.rb を更新するだけで十分です。config.ru を更新する必要はありません。

config.ru では、ブロックを use コマンドに渡すことで Rack::Cors を設定してください。

use Rack::Cors do
  allow do
    origins 'localhost:3000', '127.0.0.1:3000',
            /\Ahttp:\/\/192\.168\.0\.\d{1,3}(:\d+)?\z/
            # 正規表現が使用可能です

    resource '/file/list_all/', :headers => 'x-domain-token'
    resource '/file/at/*',
        methods: [:get, :post, :delete, :put, :patch, :options, :head],
        headers: 'x-domain-token',
        expose: ['Some-Custom-Response-Header'],
        max_age: 600
        # 公開するヘッダー
  end

  allow do
    origins '*'
    resource '/public/*', headers: :any, methods: :get

    # 指定ホストのみリクエストを許可する
    resource '/api/v1/*',
        headers: :any,
        methods: :get,
        if: proc { |env| env['HTTP_HOST'] == 'api.example.com' }
  end
end

設定リファレンス

ミドルウェア オプション

  • debug (boolean): デバッグロギングとデバッグ用の HTTP ヘッダ X-Rack-CORS を有効にします。
  • logger (Object or Proc): proc が与えられた場合は、ロガーが必要なときに proc が呼び出されます。Rails.logger のように Rack::Cors が最初に設定された後にこのオプションは役に立ちます。

オリジン

オリジンには、ストリング、正規表現、「*」(すべてのオリジンを許可) を指定することができます。

セキュリティ上の注意: 正規表現を使用するときは、誤って包括的にならないように注意してください。たとえば、正規表現 /https:\/\/example\.com/ は example.com.randomdomainname.co.uk というドメインにマッチします。正規表現は、開始文字列と終了文字列のアンカー(\A\z)で囲むことをお勧めします。

さらに、下記の形式のブロックを使用することでオリジンを動的に指定することもできます。

  origins { |source, env| true || false }

リソースパスは、正確な文字列のマッチ(/path/to/file.txt) あるいはワイルドカード(/all/files/in/*) として指定することができます。ディレクトリとそのサブディレクトリのファイルをすべて含めるためには、 /assets/**/* の形式を使用してください。リソースは次のオプションを受け取ります。

  • methods (ストリング、配列、:any): リソースに対して許可される HTTP メソッド。
  • headers (ストリング、配列、:any): CORS のリソースリクエストにおいて許可される HTTP ヘッダ。実際のリクエストですべてのヘッダを許可するには :any を使用してください。
  • expose (ストリング、配列): クライアントに公開されるリソースレスポンスの HTTP ヘッダ。
  • credentials (boolean, デフォルト: false): Access-Control-Allow-Credentials レスポンスヘッダを設定します。注意:もしオリジンにワイルドカード (*)を指定したときは、このオプションを true に設定することはできません。詳細は、 security article をご覧ください。
  • max_age (数値): Access-Control-Max-Age レスポンスヘッダを設定します。
  • if (Proc): proc の結果が true のとき、有効な CORS リクエストとしてリクエストを処理します。
  • vary (ストリング、配列): Varyヘッダに追加する HTTP ヘッダのリスト。

共通の落とし穴

ミドルウェアスタック内でRack::Cors を正しく配置しないと、予想外の結果が出ることがあります。上述の Rails の例では、すべてのミドルウェアの一番上に Rack::Cors を配置しています。これにより、ほとんどの問題を防ぐことができます。

よくあるケースを紹介します。

  • 静的ファイルの配信。 適切な CORS ヘッダと共に静的ファイルが提供されるように、ActionDispatch::Static の前に Rack::Cors を設定してください(下記の注意書きをご覧ください)。注意: 通常、静的ファイルはWebサーバ(Nginx, Apache)から提供され、Rails コンテナからは提供されないため、本番環境では動作しないかもしれません。

  • ミドルウェアのキャッシュ。 一度キャッシュされたものではなく、適切な CORS ヘッダが記載されるように、Rack::Cache の前に Rack::Cors を設定してください。

  • Wardenによる認証。 認証を必要とするリソースに認証なしでアクセスされた場合、Warden はすぐに動作します。もしスタックの中で Rack::Cors より前に Warden::Manager がある場合、正しい CORS ヘッダが適用されないまま Warden は動作します。必ず Warden::Manager の前に Rack::Cors を設定するようにしてください。

Rack スタックにおいて CORS ミドルウェアをどこに配置するかを決めるためには、下記のコマンドを実行してください。

bundle exec rake middleware

多くの場合、Rack スタックは本番環境では異なる動作をしています。たとえば、config.serve_static_assets = false のとき、ActionDispatch::Static ミドルウェアはスタックには入っていません。本番環境でミドルウェアスタックがどのようになっているかを確認するためには、下記のコマンドを実行してください。

RAILS_ENV=production bundle exec rake middleware
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む