rails全般

はじめに

  • コマンドの説明は、#だとroot権限での実行とまぎらわしいので、 //で記載。

リンク

その他

bundle install --path vendor/bundle --binstubs=vendor/bin の実行方法

  • 古い実行方法
$ bundle install --path vendor/bundle --binstubs=vendor/bin
  • 新しい実行方法
$ bundle config --local path 'vendor/bundle'
$ bundle binstubs --path=vendor/bin

// $ bundle binstubs --path=bin としてしまうと、rails app:update:bin で生成されるものとぶつかるので注意。
  • bundle config
// 確認
$ bundle config
$ bundle config <name>

// 設定(global)
$ bundle config <name> <value>
$ bundle config --global <name> <value>

// 設定(local)
$ bundle config --local <name> <value>

// 削除
$ bundle config --delete

用語

  • アクション・・・publicなインスタンスメソッド

初期設定

rails new

// --skip-test-unit・・・RSpec等を利用する場合に、Test::Unitの設定をスキップ
// --skip-bundle・・・gemのinstallをスキップ(デフォルトのGemfileを変更してからinstallしたい場合。)
$ rails new $APP_NAME$ -d postgresql --skip-test-unit
$ rails new $APP_NAME$ -d mysql --skip-test-unit

configure Gemfile

$ vim Gemfile
$ ./bin/bundle
  • Gemfileの例
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "2.6.4"

gem "rails", "~> 6.0.3", ">= 6.0.3.2"
gem "pg", ">= 0.18", "< 2.0"
gem "puma", "~> 4.1"
gem "sass-rails", ">= 6"
gem "webpacker", "~> 4.0"
gem "turbolinks", "~> 5"
gem "jbuilder", "~> 2.7"
gem "bootsnap", ">= 1.4.2", require: false


### ===additional utility===start
gem "bcrypt" # for password encryption
gem "rails-i18n" # for multilingualization
gem "kaminari" # for pagination
gem "date_validator" # for date validation
gem "valid_email2" # for email validation
gem "nokogiri" # xml/html parser/generator
### ===additional utility===end

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

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

group :test do
  gem "capybara", ">= 2.15"
  gem "selenium-webdriver"
  gem "webdrivers"

  ### for rspec
  gem "rspec-rails"
  gem "factory_bot_rails"
end

yarn install

$ yarn

// インストール済みのファイルが削除されていないか確認する場合は、
// yarn install --check-files

db生成

  • デフォルト設定のpostgresを使う場合の、 config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: db
  username: postgres
  password: ""
  • db生成
./bin/rails db:create

タイムゾーンとロケールの設定

class Application < Rails::Application
  config.load_defaults 6.0

+    config.time_zone = "Tokyo"
+    # "{ROOT}/config/locales/**/*.{rb,yml}"
+    config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}").to_s]
+    config.i18n.default_local = :ja
end

ジェネレータの設定

class Application < Rails::Application
  config.load_defaults 6.0

  config.time_zone = "Tokyo"
  # "{ROOT}/config/locales/**/*.{rb,yml}"
  config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}").to_s]
  config.i18n.default_locale = :ja

+  config.generators do |g|
+    g.skip_routes true
+    g.helper false
+    g.assets false
+    g.test_framework :rspec # mini_test -> RSpec
+    g.controller_specs false
+    g.view_specs false
+  end
end

Blocked Hosts

  • Rails6からの機能
  • アクセスできるドメインを制限するための、許可リスト
    • デフォルトは、localhostのみが許可
  • config/initializers/blocked_hosts.rbを新規作成する
Rails.application.configure do
  config.hosts << "example.com"
  config.hosts << "hoge.example.com"
end
// rails側のコードでは、次のように開発環境においてはlocalhostがデフォルトで許可されている。
@hosts = Array(([".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")] if Rails.env.development?))
  • blocked hostsを無効にする場合は、次のようにnilを入れる
Rails.application.configure do
  config.hosts = nil
end

web-console

  • rails用のデバッグツール
  • デフォルトは、127.0.0.1からのアクセスのみを受け付ける。
  • web-consoleが受け付けない範囲のIPからのアクセスの場合、ログにCannot render console from xx.xx.xx.xxとでる。
  • config/environments/development.rbを次のように編集することでIP範囲を広げる
  config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+  
+  # allow from 172.16.0.0 to 172.31.255.255
+  config.web_console.whitelisted_ips = ["172.16.0.0/12"] 
end

railsの起動

  • すべてのIPアドレスから受け付ける場合は、./bin/rails s -b 0.0.0.0

RSpecの初期設定

  • 次を1度実行すれば、RSpecに必要な初期ファイルが生成される。
./bin/rails g rspec:install
> create  .rspec
> create  spec
> create  spec/spec_helper.rb
> create  spec/rails_helper.rb

Specファイルのディレクトリの慣習

  • モデルに関するもの
    • spec/models 以下
  • APIに関するもの
    • spec/requests 以下

テスト環境自体のテスト(確認)

  • 仮でテストを作成
    • spec/sample/string_spec.rb
require 'spec_helper'

describe 'String' do
  before do
    # Do nothing
  end

  after do
    # Do nothing
  end

  describe '#<<' do
    example '文字の追加' do
      s = "ABC"
      s << "DEF"
      expect(s.size).to eq(6)
      expect(s).to eq("ABCDEF")
    end

    example 'nilの追加(pending)' do
      pending "pendingなので、failureにカウントされない。"
      s = "ABC"
      s << nil
      expect(s.size).to eq(3)
      expect(s).to eq("ABC")
    end

    xexample 'nilの追加(スキップ)' do
      s = "ABC"
      s << nil
      expect(s.size).to eq(3)
      expect(s).to eq("ABC")
    end
  end
end
$ bundle exec rspec spec/sample/string_spec.rb

// bundle exec rspec と同じ 
$ bundle exec rspec spec

// 行番号指定で、特定のテストだけ実行
$ bundle exec rspec spec/sample/string_spec.rb:25

$ bundle exec rspec --tag=tag1

開発1

ルーティングとコントローラーの生成

  • config/routes.rb
Rails.application.routes.draw do
  namespace :staff do
    # /staff
    # Staff::TopController
    root "top#index"
  end

  namespace :admin do
    # /admin
    # Admin::TopController
    root "top#index"
  end

  namespace :customer do
    # /customer
    # Customer::TopController
    root "top#index"
  end
end
$ ./bin/rails g controller staff/top
$ ./bin/rails g controller admin/top
$ ./bin/rails g controller customer/top

> create  app/controllers/staff/top_controller.rb
> invoke  erb
> create    app/views/staff/top
> invoke  rspec
> create    spec/requests/staff/top_request_spec.rb

シンプルなコントローラー

  • app/controllers/staff/top_controller.rb
class Staff::TopController < ApplicationController

  # レスポンスを返すメソッドを呼ばない場合、
  # デフォルトでアクションに対応するERBテンプレートが使用されるので、
  # 下記でも同じ
  # def index
  # end
  #
  def index
    render action: "index"
  end
end
  • app/views/staff/top/index.html.erb
<% @title = "職員トップページ" %>
<h1><%= @title %></h1>

ERB1

-

  • 単純にrubyを実行する
<%  >
  • rubyを評価し、文字列として出力する
<%=  >
  • ERBにてエスケープ処理をさせたくない。
    • ActiveSupport::SafeBuffer を利用する
      • ERBに埋め込まれる際にエスケープされない文字列クラス。
        • #html_safe か raw() で利用する
<%= @str.html_safe %>
<%= raw(@str) %>

-

  • stylesheet_link_tag
    • CSS用のタグ
    • app/assets/stylesheetsディレクトリを見る。
    • オプション
      • media: all(すべて)、print(印刷物)、screen(スクリーン)等がある。
  • javascript_pack_tag
    • javascript用のタグ
    • app/javascripts/packsディレクトリを見る。
    • オプション
      • data-turbolinks-track: turbolinks(画面遷移高速化に必要)を有効・無効にする。

-

  • 部分テンプレート
    • erbでrenderメソッドを使うと、引数の文字列を部分テンプレートの文字列であると判断する。
      • ファイル名は、_をつけたものになる。(ex: 引数 header、ファイル名 _header.html.erb)

ERBヘルパーメソッドの定義方法

  • app/helpers/application_helper.rbApplicationHelperモジュールのメソッドとして定義する。

アセットパイプライン

  • 対象
    • app/assets/images (画像)
    • app/assets/javascripts (javascripts)
    • app/assets/stylesheets (css)
  • 効果
    • app/assets/stylesheets 配下のscssをcssに変換して結合等。
  • コンパイル
    • ./bin/rails assets:precompile
      • public/assets以下にCSSやJavaScriptが生成される。abc-XXXXX.cssのようにXXXXXに32桁のフィンガープリントが入る(値はファイルの中身のMD5)。ファイル更新時に違うファイルがブラウザにキャッシュされ続けないようにするための対策。

スタイルシート

  • スタイルシートのコメント内の *=
    • railsはディレクティブとして解釈する。
    • cssディレクティブ例 - require_tree . ・・・ app/assets/stylesheets以下のファイルをすべてアセットパイプラインの処理範囲とする。
      • require_self 自身もアセットパイプラインの処理対象とする。

環境ごとに異なるcss/htmlを用意する場合。

  • css
    • 環境ごとのcssファイルを作成する。
      • staff.css / admin.css
    • それぞれのcssに異なる、require_treeを用意する。
      • require_tree ./staff / require_tree ./admin
    • config/initializers/assets.rb にコンパイル設定を追加
      • Rails.application.config.assets.precompile += %w( staff.css admin.css )
  • htmlを分ける
    • app/views/layoutsapplication.html.erb相当のものを作る。(application.html.erbは不要なら削除)
      • staff.html.erb / admin.html.erb
    • erb内の、cssの向き先(link)を変更
      • <%= stylesheet_link_tag 'staff', media: 'all', 'data-turbolinks-track': 'reload' %>
      • <%= stylesheet_link_tag 'admin', media: 'all', 'data-turbolinks-track': 'reload' %>
  • app/controllers/application_controller.rbでのerbの呼び出し先を変更
// app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  layout :set_layout

  private def set_layout
    if params[:controller].match(%r{\A(staff|admin|customer)/})
      Regexp.last_match[1]
    else
      "customer"
    end
  end
end
  • ERBテンプレートのデフォルトの呼び出し(ex: Staff::TopControllerのアクションの場合。)
    • 第1候補: app/views/layouts/staff/top.html.erb
    • 第2候補: app/views/layouts/application.html.erb
  • ERBテンプレートのデフォルトの呼び出しを変更する。
    • 上記のように、ApplicationControllerのlayoutでおこなう。
  • ApplicationControllerとは?
    • すべてのコントローラーの親クラス。ベースクラス。

SCSS(Sass)

環境

  • 環境(3つ)
  • test
  • development
  • production
    • ソースコードが書き換わってもアプリケーションはリロードしない。起動時の状態で動作を続ける。
      • config/environments/production.rbにて、config.cache_classes=falseとすればリロードするようになる。
    • Rails自身がアセット(画像/Javascript/CSS)をブラウザに返さない。
      • ApacheやNginxで返す。
      • ただし、export RAILS_SERVE_STATIC_FILES=1 の様にRAILS_SERVE_STATIC_FILESに何らかの値を設定すればRailsアプリケーションから返却するようになる。
  • developmentモード
    • ./bin/rails s はデフォルトのdevelopmentモードになる。
  • productionモード
    • 引数に RAILS_ENV=production をつけると、productionモードになる。
      • ./bin/rails db:create RAILS_ENV=production
      • ./bin/rails s -e production -b 0.0.0.0 でもproductionモードになる。

資格情報の暗号化

ファイル

  • credentials.yml.enc
    • 暗号化された資格情報
  • master.key
    • 復号化用のkey
    • .gitignoreで除外する

credentials.yml.enc と master.key の組み合わせの確認。

  • 次を実行して編集できるならば問題ない。
    • Couldn't decrypt config/credentials.yml.enc. Perhaps you passed the wrong key?と出た場合は、組み合わせが正しくないので、2ファイルとも消して作り直す必要がある。
EDITOR=vim ./bin/rails credentials:edit

例外処理

  • 例外のraise
// RuntimeErrorをraise
raise

//
raise ArgumentError, "the first argument must be a string"
  • 例外処理
begin
  A
rescue E1 => e1
  C1
rescue E2 => e2
  C2
ensure
  Z
end
  • rescue_from
    • アクション内で発生した例外のキャッチ
    • 複数のrescue_fromを記述する場合は、例外クラスの"先祖"が先の行、“子孫"が後の行に来るようにする必要がある。
class ApplicationController 

  rescue_from ActiveRecord::RecordNotFound, with: :rescue403

  private def rescue403(e)
    @exception = e
    # パスに / が含まれているとコントローラーの階層とは別のところにviewがあると判断される。
    # (Rails 2.2以前では、template: オプションが必須だった)
    render template: "errors/forbidden", status: 403
  end

end

ログの場所

  • production
    • log/production.log

マイグレーション

  • t.string :email
    • t.column :email, :string の意
    • ActiveRecord::ConnectionAdapters::TableDefinition
  • add_index
      • add_index :TABLE_NAME, “LOWER(email)”, unique: true
      • add_index :TABLE_NAME, [ :COLOMN_NAME1, :COLOMN_NAME2 ]
    • “LOWER(email)” は関数indexなので、postgressは使えるが、mysqlは使えない。(“lower(email)“でも可)
      • lower(email) の場合は、lower関数を適用してからindexに格納されることになる。
      • デフォルトで、postgressは大文字小文字を区別する。mysqlは区別しない。

コマンド

// マイグレーションを実行(ここで、db/schema.rbが自動更新される。)
$ ./bin/rails db:migrate

// データベースを削除した上で、新たにデータベースし直して、migrateを実行
$ ./bin/rails db:migrate:reset

// すべてのテーブルをdropし、db/schema.rbの内容でテーブルを作り直し、seedデータを投入(db:schema:load -> db:seed)
// seedデータを投入し直したい場合は、このコマンド
$ ./bin/rails db:reset
// テーブルがどんなカラムを持っているかを調べる (./bin/rails runner)
$ ./bin/rails r "ModelName.columns.each {|c| p [ c.name, c.type ] }"

db/schema.rbを使ったdbの初期化

  • edit config/database.yml
  • ./bin/rails db:setup
    • このコマンドは次のコマンドを順次実行するのと同じ。
      • ./bin/rails db:create
      • ./bin/rails db:shema:load
      • ./bin/rails db:seed

モデル

  • テーブル名に独自のものを使う場合、カラム名に別名を使いたい場合
class ModelName < ActiveRecord::Base
  self.table_name = "TableName"
  alias_attribute :email, :OriginalColumnName
end

// model.email or model.OriginalColumnName でアクセス可

メソッド

  • persisted?
    • データベースに保存されていればtrueをそうでなければfalseを返す
      • フォームのヘルパーメソッド、form_withはこのメソッドの戻り値によってHTTPメソッドをPOSTにするかPATCHにするか決めている。
      • 非ActiveRecordモデル(include ActiveModel::Model しただけのクラス)は、デフォルトでfalseを返却する。

シード

$ ./bin/rails db:seed

// ./bin/rails r "puts Model.count" 等で確認。

helper_method

  • 前提
    • コントローラーのインスタンス変数は、viewからも参照できる。
    • コントローラーのインスタンス変数を利用するメソッドを、ApplicationHelperに定義してviewからそのメソッドを呼び出しても問題ない。
    • コントローラーに定義されたメソッドをApplicationHelperにコピーする代わりに、Controller側でhelper_method :method_name_xxxのように指定すれば、ApplicationHelperのメソッドとしても定義される。

Railsの基本アクション

Action名 対応するHTTPメソッド
index GET
show GET
new GET
edit GET
create POST
update PATCH
destroy DELETE

フォーム

  • form_tag・・・HTMLのformタグの生成のみ
  • form_for・・・オブジェクトを引数にとり、そのオブジェクトの属性値を各入力欄のデフォルト値に設定する。
  • form_with・・・form_tagとform_forを統合したもの (Rails5.1より)
    • form_tagとform_forは将来的に非推奨にしたい旨が言及されている。

formオブジェクト

formオブジェクトのメソッド
  • fields_for
    • <%= f.fields_for :bar_baz, f.object.foo.bar_baz do |ff| %>
      • レコード名、オブジェクトを引数に取り、formオブジェクトの対象となるモデルオブジェクトを切り替える。
      • この例の場合、送信値はparams[:foo_bar_model_name][:bar_baz] で取得できる。[:bar_baz]の部分は、レコード名として指定した値が入る。

form_with

  • デフォルトでは、リモートフォームが有効になっている。(Ajaxリクエストを送受信するのみで、画面表示の更新は自分でJavaScriptを記述する)
  • リモートフォームを無効にするには、config/initializers/action_view.rbを下記のようにする。
Rails.application.configure do
  config.action_view.form_with_generates_remote_forms = false
end
  • HTTPメソッドの呼び出しは
    • modelオプションに指定したオブジェクトがデータベースに保存されているかどうかできまる。(id属性に値があるかどうか)
      • 保存されていない・・・POST
      • 保存されている・・・PATCH
form_withのオプション
  • scope
    • prefixとなる名前を変更できる。
      • form_withによって送信されるフィールド名は、例えばMyUserモデルのemailフィールドなら、my_user[email]で、取得する時は、params[:my_user][:email]となる、このmy_userがprefix。

Strong Parameters (ストロングパラメータズ)

  • formからの送信値の内、指定したパラメータだけが保存(db)処理にまわされる安全機構。
  • offにするには、例えばconfig/initializers/action_controller.rbを作り、下記のようにする.
Rails.application.configure do
  config.action_controller.permit_all_parameters = true
end

formの例

<%= form_with model: @user, url: :registration do |f| %>
  <%= f.label :name, "名前" %>
  <%= f.text_field :email %>
  <%= f.submit "送信" %>
<% end %>

formオブジェクトの例

  • include ActiveModel::Model によって、form_withのmodel引数に渡すことができるようになる。
class Staff::LoginForm
  include ActiveModel::Model

  attr_accessor :email, :password
end

params

  • ActionController::Parametersクラスのインスタンス
  • Staff::LoginForm の値は次のように格納される
staff_login_form: {
  email: "aaaaa@example.com",
  password: "foo"
}

サービスオブジェクト

ビジネスロジック実行用・集約用の場所

  • app/services/staff/authenticator.rb を以下のようにすると
class Staff::Authenticator
  def initialize(staff_member)
    @staff_member = staff_member
  end

  def authenticate(raw_password)
    @staff_member &&
        !@staff_member.suspended? &&
        @staff_member.hashed_password &&
        @staff_member.start_date <= Date.today &&
        (@staff_member.end_date.nil? || @staff_member.end_date > Date.today) &&
        BCrypt::Password.new(@staff_member.hashed_password) == raw_password
  end
end
  • 次のように呼び出せる。
Staff::Authenticator.new(staff_member).authenticate(@form.password)

ActiveSupport::Concern

  • ActiveSupport::Concern
    • コード共有化の仕組み
    • app/controllers/concerns にファイルを作成
      • コントローラで使用するモジュールを配置するフォルダ
    • extend ActiveSupport::Concernの記述をする。
      • includedメソッドが利用可能になる。
        • ブロックを取り、ブロック内のコードがモジュールを読み込んだクラスの文脈で評価される。
      • モジュールのサブクラスとしてClassMethodsというクラスを定義しておくと、そのメソッドがモジュールを読み込んだクラスのクラスメソッドとして取り込まれる。
module ErrorHandlers
  extend ActiveSupport::Concern

  included do
    rescue_from StandardError, with: :rescue500
    rescue_from ApplicationController::Forbidden, with: :rescue403
    rescue_from ApplicationController::IpAddressRejected, with: :rescue403
    rescue_from ActiveRecord::RecordNotFound, with: :rescue404

    private def rescue403(e)
      @exception = e
      render "errors/forbidden", status: 403
    end

    private def rescue404(e)
      render "errors/not_found", status: 404
    end

    private def rescue500(e)
      render "errors/internal_server_error", status: 500
    end
  end
end

Factory Bot

  • テスト用に、データベースにテストデータを投入するためのもの

設定

  • spec/rails_helper.rb に次を追加
config.include FactoryBot::Syntax::Methods

ファクトリーの定義例

  • spec/factories/staff_members.rb
FactoryBot.define do
  factory :staff_member do
    sequence(:email) { |n| "member#{n}@example.com" }
    family_name { "山田" }
    given_name { "太郎" }
    family_name_kana { "ヤマダ" }
    given_name_kana { "タロウ" }
    password { "pw" }
    start_date { Date.yesterday }
    end_date { nil }
    suspended { false }
  end
end

ファクトリーのメソッド

  • build
    • インスタンスを作るだけ
  • create
    • インスタンスを作り、データベースに保存
  • attributes_for
    • ファクトリーの名前を引数にとって、ハッシュを返却する。
      • このハッシュは、assign_attributesの引数として使用できるキーと値を持つ

ファクトリーを使ったテストの例

  • spec/services/staff/authenticator_spec.rb
require 'rails_helper'

describe Staff::Authenticator do
  describe "#authenticate" do
    example "正しいパスワードならtrueを返す" do
      m = build(:staff_member)
      expect(Staff::Authenticator.new(m).authenticate("pw")).to be_truthy
    end

    example "誤ったパスワードならfalseを返す" do
      m = build(:staff_member)

      expect(Staff::Authenticator.new(m).authenticate("xy")).to be_falsey
    end

    example "パスワード未設定ならfalseを返す" do
      m = build(:staff_member, password: nil)

      expect(Staff::Authenticator.new(m).authenticate(nil)).to be_falsey
    end

    example "停止フラグが立っていればfalseを返す" do
      m = build(:staff_member, suspended: true)

      expect(Staff::Authenticator.new(m).authenticate("pw")).to be_falsey
    end

    example "開始前ならfalseを返す" do
      m = build(:staff_member, start_date: Date.tomorrow)

      expect(Staff::Authenticator.new(m).authenticate("pw")).to be_falsey
    end

    example "終了後ならfalseを返す" do
      m = build(:staff_member, end_date: Date.today)

      expect(Staff::Authenticator.new(m).authenticate("pw")).to be_falsey
    end
  end
end

フラッシュ

  • クライアントごとに一時的にデータを保持する仕組み
    • フラッシュオブジェクトは、1回目のアクセスで記録されたデータは、2回目のアクセスまでしか維持されない。(3回目のアクセスでは消えている。)
  • フラッシュオブジェクト
    • 属性は2つ
      • flash.notice ・・・普通のメッセージ
      • flash.alert ・・・警告メッセージ
    • flash.now
      • flash.now.alert のように nowをつけると、そのアクションが終われば消えるようになる。
        • flash.now はセットのときだけ意味があり、参照時には、flash.notice / flash.alertでよい。
    • flash.discard / flash.keep
      • flash.discard は、指定したキーをflash.nowで宣言したときと同じ状態にする。
      • flash.keepは、次のアクションまで保持される。
// flash.notice
renderでは消えないが、redirectをするとアクションを経由するので、表示後に消える。

// flash.now.notice
render後に消える。

// flash.keep.notice
redirectを2回経由したら、表示後に消える。

ルーティング(routes)

  • パラメータの制限付きのルーティング例
Rails.application.routes.draw do
  get "blog/:year/:month/:mday" => "articles#show",
    constraints: {
      year:/20\d\d/,month:/\d\d/,mday:/\d\d/
    }
end
  • ルーティングの名前付けと、link_toでのリンク生成例
Rails.application.routes.draw do
  get "staff/login" => "staff/sessions#new", as: :staff_login
end

<%= link_to "ログイン", :staff_login %>
(<%= link_to "ログイン", "/staff/login" %>)
  • ルーティングの名前付けが、URLパスのパターンであり、HTTPメソッドまでは含んでいない例
Rails.application.routes.draw do
  get "login" => "sessions#new", as: :login_form
  post "login" => "sessions#create", as: :authentication
end

<%= link_to "ログイン", :login_form %>
(<%= link_to "ログイン", "/login" %>)

<%= link_to "ログイン", :authentication %>
(<%= link_to "ログイン", "/login" %>)
  • ルーティングに名前を付けた場合に定義されるヘルパーメソッド
  • “login” というパスにつけた名前が、 :login だった場合
    • login_path
    • login_url
<%= link_to "ログイン", :login %>
(<%= link_to "ログイン", "/login" %>)

<%= link_to "ログイン", :login_path %>
(<%= link_to "ログイン", "/login" %>)

<%= link_to "ログイン", :login_path(tracking: "001") %>
(<%= link_to "ログイン", "/login?stacking=001" %>)
  • オブジェクトや配列でリンク生成
    • なお、 _urlヘルパーは、_pathの前に現在のホスト名、ポート番号、パスのプレフィックスが追加されている
    • link_to に配列を渡した時のURLマッチの仕様はredirect_to とかも同じ
// /magazines/:magazine_id/ads/:id というroutingがあり、
// MagazineモデルとAdモデルのインスタンスがあるとする
// @magazine のidが2, @ad のidが3であるとする
@magazine, @ad


// /magazines/2/ads/3 を示す。
<%= link_to 'Ad details', magazine_ad_path(2, 3) %>

// オブジェクトのidを利用させることもできる
// /magazines/2/ads/3 を示す。
<%= link_to 'Ad details', magazine_ad_path(@magazine, @ad) %>


// url_forは、配列・setから適切なurlを生成します。
// この場合は、MagazineモデルとAdモデルなので、
// Magazine_Ad_path をlowcaseにした。magazine_ad_path(@magazine, @ad)を呼び出します。
// もし、StaffMemberモデルの場合は、staff_member_path() です。
<%= link_to 'Ad details', url_for([@magazine, @ad]) %>

// url_forに、配列・setを渡すかわりに、url_forを省略して配列を渡しても同じです。
<%= link_to 'Ad details', [@magazine, @ad] %>

// オブジェクトだけを渡した場合は、
// magazine_path(@magazine) と同じです。
<%= link_to 'Ad details', @magazine %>

// 次の配列は、:edit, :foo を追加で渡しているので、
// magazine_ad_path(@magazine, @ad) に:edit, :fooをつけた
// edit_foo_magazine_ad_path(@magazine, @ad) が呼び出されることになります。
<%= link_to 'Edit Ad', [:edit, :foo, @magazine, @ad] %>
  • ルーティングに名前を付けた場合に定義されるヘルパーメソッド (2)
    • path内にパラメータがある場合は、xxx_pathメソッドにはパラメータを与える必要がある。
Rails.application.routes.draw do 
  get "articles/:year/:number" => "articles#show", as: :article
end

<%= link_to "読む", article_path(year: "2019", number: "12") %>
  • link_to
    • 第1引数: 表示する文字列
    • 第2引数: url
    • remote: ajaxでリクエストするかの制御、trueならajax
  • link_to_unless
  • link_toに加え、第1引数に表示するかどうかの条件式を取る。
  • ブロックをつけると、第1引数がfalseのときにの文字列を指定できる。
  • <%= link_to_unless( ........ ) do |name| ...... end %>

名前空間

  • 次のようにnamespaceを設定した場合の効果は、
    • URLパスの先頭に /staff が付加
      • これを変えるオプションは、 path:
    • コントローラー名の先頭に staff/ が付加
      • これを変えるオプションは、 module:
    • ルーティング名の先頭に staff_ が付加
      • これを変えるオプションは、 as:
Rails.application.routes.draw do
  namespace :staff do
    ......
  end
do

// オプション適用時
Rails.application.routes.draw do
  namespace :staff, path: "bar", module: "bar", as: "bar" do
    ......
  end
do

リソースベース(複数)・・・resources

  • 用語
    • メンバールーティング
      • /foos/:id のように要素に対するルーティング
    • コレクションルーティング
      • /foos のように集合に対するルーティング
    • /foos/new は、メンバー・コレクションルーティングのどっちとも言えない。
内容 HTTPメソッド URLパターン アクション名
リスト画面表示 GET /foos index
詳細画面表示 GET /foos/:id show
登録画面表示 GET /foos/new new
編集画面表示 GET /foos/:id/edit edit
追加 POST /foos create
更新 PATCH /foos/:id update
削除 DELETE /foos/:id destroy
  • resources
    • 上記7つのルーティングを行う。
    • ルーティング名も次のように自動で設定される。
URLパターン ルーティング名
/foo/bar_hoges :foo_bar_hoges
/foo/bar_hoges/:id :foo_bar_hoge
/foo/bar_hoges/new :new_foo_bar_hoge
/foo/bar_hoges/:id/edit :edit_foo_bar_hoge
  • resourcesのオプション
    • only
      • 指定したアクションだけを設定する。
        • resources :foo_bars, except: [:index, :new, :create]
    • except
      • 指定したアクションを除外して設定する。
        • resources :foo_bars, except: [:index, :new, :create]
    • controller
      • controllerだけを変更する。
        • resources :foo_bars, controller: "hoo_bars"
    • path
      • pathだけを変更する。
        • resources :foo_bars, path: "hoo_bars"

リソースベース(単数)・・・resource

  • 用語
    • 単数リソース(singular resource)
      • 文脈上、存在が1つのリソース
内容 HTTPメソッド URLパターン アクション名
詳細画面表示 GET /foo show
登録画面表示 GET /foo/new new
編集画面表示 GET /foo/edit edit
追加 POST /foo create
更新 PATCH /foo update
削除 DELETE /foo destroy
  • resource
    • メンバールーティング・コレクションルーティングといった分類はない。
    • 注) resource :account としても、コントローラー名は accounts となる。
    • 上記6つのルーティングを行う。
    • ルーティング名も次のように自動で設定される。
URLパターン ルーティング名
/foo :foo
/foo/new :new_foo
/foo/edit :edit_foo

configオブジェクト

  • config/initializersに新規ファイルを作って、以下のように任意のkeyでconfigオブジェクトに値を設定できる。
Rails.application.configure do
  config.foo = {
      bar: {hoge: "foobar"}
  }
end

制約 (constraints)

  • 次のようにhost名で制限をかけられる。
Rails.application.routes.draw do
  config = Rails.application.config.foo

  constraints host: "foo.example.com" do
    namespace :foo do
    end
  end

  constraints host: config[:bar][:hoge] do
    namespace :bar do
    end
  end
end

model

  • object.new_record?
    • データベースに保存済みかどうかを確認します
// パラメータを受け取って保存
@staff_member = StaffMember.new(params[:staff_member])
@staff_member.save // true or false

// 値を一括で設定
@staff_member.assign_attributes(params[:staff_member])
// assign_attributes と同じ (attributes= は、assign_attributesのエイリアス)
@staff_member.attributes = params[:staff_member]

部分テンプレート

-

  • ファイル名の前にアンダースコアをつける必要がある

部分テンプレートでの_form作成例

<div class="legend">
  <span class="mark">*</span> 印の付いた項目は入力必須です。
</div>
<div>
  <%= f.label :email, "メールアドレス", class: "required" %>
  <%= f.email_field :email, size: 32, required: true %>
</div>
<% if f.object.new_record? %>
  <div>
    <%= f.label :password, "パスワード", class: "required" %>
    <%= f.password_field :password, size: 32, required: true %>
  </div>
<% end %>
<div>
  <%= f.label :family_name, "氏名", class: "required" %>
  <%= f.text_field :family_name, required: true %>
  <%= f.text_field :given_name, required: true %>
</div>
<div>
  <%= f.label :start_date, "入社日", class: "required" %>
  <%= f.date_field :start_date, required: true %>
</div>

<div class="check-boxes">
  <%= f.check_box :suspended %>
  <%= f.label :suspended, "アカウント停止" %>
</div>
  • classにrequiredを指定したラベルの右側に * を表示するscss
form {
  div {
    label.required:after {
      content: "*"; 
      padding-left: $narrow;
      color: $red;
    }
  }
}

部分テンプレートの繰り返し呼び出し

<%= render partial: "event", collection: @events %>

と書いた場合、@eventsの要素数分だけ繰り返し部分テンプレートが呼び出される。 そのときに渡される変数名は、event。

strong parameters

  • strong parametersが有効な場合、params#require#permitとフィルターを通していないものを、モデルオブジェクトのassign_attributesで渡すと、ActiveModel::ForbiddenAttributesErrorがraiseされる。
  • params.require(:hoge)
    • paramsオブジェクトに:hogeがなければ、ActionController::ParameterMissingをraiseする。
  • params.permit(:email)
    • paramsオブジェクトから、:email以外のパラメータを除去したparamsオブジェクトを返却する。

rspec 2

メソッド

  • let
    • let(:foo) { bar()}
      • メモ化を行うメソッド、引数で渡したキーで次の性質をもつメソッドとして呼び出せるようになる。
        • 1回目は普通に処理を実行して結果を保持する。
        • 2回目以降は、処理を実行せずに1回目の実行結果を返却する。
  • let!
    • letと同じだが、定義時に実行される点だけ異なる。
  • get, post
    • 内部で擬似的にHTTPリクエストを行い、アクションを実行する。
  • response
    • ActionController::TestResponse オブジェクトを返却する。アクションの実行結果に関する情報を保持している。
  • beforeブロック
    • 各exampleの実行前に実行される。

マッチャー

マッチャー・・・expect().to hoge のhogeの部分

  • redirect_to
    • リダイレクションが発生したか?を確認する
      • urlではなく、pathのみを指定した場合、test.hostというホスト名のURLが生成され、このURLでテストされる。
  • be_hoge (beマッチャー)
    • be_で始まるマッチャーが未定義の場合、be_を外したメソッド名に?をつけたもの (hoge?) がターゲットオブジェクトに対して呼ばれる。そしてそれがtrueならテストが成功となる。

テストパターン

  • expect { X }.not_to change { Y }
    • X実行前後に1回ずつYを実行し、結果が変わっていなければ成功。結果同士の比較は==メソッドで行われる。

Controller

  • before_action
    • 各アクションの実行前に呼び出される。
  • skip_before_action
    • 指定した名前のメソッドが、アクションの前に実行されないようにする。

時刻

  • 60.minutes
    • ActiveSupport::Duration
  • 60.minutes.ago
    • 60分前の時刻の、Time or Date
  • 60.minutes.from_now
    • 60分後の時刻の、Time or Date
  • 60.minutes.from_now.advance(seconds: 1)
    • 60分1秒後の時刻の、Time or Date

テスト

共有イグザンプル(shared examples) ・・・ テストの共通化

  • spec/support に作成する
  • spec/rails_helper.rbにて、spec/supportを自動で読み込むようにする。
    • 自動生成されたrails_helper.rbには、コメントアウトされて自動読み込みのためのコードが記されている
// spec/supportを自動で読み込むようにするコード

require 'rspec/rails'
+ Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

describe, context, example

  • describeの第2引数に、contextを書くことができる。
describe "管理者による職員管理", "ログイン前" do
  describe "新規登録" do
    example "職員一覧ページにリダイレクト" do
    end

    example "例外 ActionController::ParameterMissingが発生" do
    end
  end
end

は、以下と同じ

describe "管理者による職員管理" do
  context "ログイン前" do
    describe "新規登録" do
      example "職員一覧ページにリダイレクト" do
      end

      example "例外 ActionController::ParameterMissingが発生" do
      end
    end
  end
end

時間に関するテスト

  • spec/rails_helper.rbにて、ActiveSupport::Testing::TimeHelpersを読み込む。
  config.include ActiveSupport::Testing::TimeHelpers
end
  • travel_to 60.minutes.from_now
    • 内部時刻を60分後にする。

モデル

createとcreate!の違い

save,create,update save!,create!,update!
ActiveRecord::RecordInvalid(validation系) return false 例外
ActiveRecord::RecordNotSaved(callback系) return false 例外
ActiveRecord::RecordNotUnique,ActiveRecord::StatementInvalidなど 例外 例外

モデルの関連づけ

  • dependent:について
    • :destroyを指定すると、関連付けられたオブジェクトも同時にdestroyされます。
    • :deleteを指定すると、関連付けられたオブジェクトはデータベースから直接削除されます。このときコールバックは実行されません。
    • :nullifyを指定すると、外部キーがNULLに設定されます。ポリモーフィックなtypeカラムもポリモーフィック関連付けでNULLに設定されます。コールバックは実行されません。
class CreateStaffEvents < ActiveRecord::Migration[6.0]
  def change
    create_table :staff_events do |t|
      t.references :staff_member, null: false, index: false, foreign_key: true
      t.string :type, null: false
      t.datetime :created_at, null: false
    end

    add_index :staff_events, :created_at
    add_index :staff_events, [ :staff_member_id, :created_at ]
  end
end


class StaffMember < ApplicationRecord
  has_many :events, class_name: "StaffEvent", dependent: :destroy
  # 次でも同じ
  # has_many :staff_events, dependent: :destroy
  # foreign_keyについては、モデル名 StaffMember から staff_member_id と推測される。
end

class StaffEvent < ApplicationRecord
  self.inheritance_column = nil
  # t.string :type は特別な意味を持つが、self.inheritance_column = nil とすることで無効化できる。

  belongs_to :member, class_name: "StaffMember", foreign_key: "staff_member_id"
  alias_attribute :occurred_at, :created_at
end

// 以下は同じ呼び出し
// staff_member.events.create!(type:"rejected")
// StaffEvent.create!(member: staff_member, type: "rejected")

クラスの継承関係の調べ方

  • 祖先を調べる
    • ./bin/rails r "puts ApplicationRecord.ancestors"
    • ./bin/rails r "puts StaffMember.new.events.class.ancestors"

例(祖先を調べる・使えるメソッドを調べる)

1. 祖先を調べる
./bin/rails r "puts StaffMember.new.events.class.ancestors"

StaffEvent::ActiveRecord_Associations_CollectionProxy
StaffEvent::GeneratedRelationMethods
ActiveRecord::Delegation::ClassSpecificRelation
ActiveRecord::Associations::CollectionProxy
ActiveRecord::Relation
ActiveRecord::FinderMethods
ActiveRecord::Calculations
ActiveRecord::SpawnMethods
ActiveRecord::QueryMethods
ActiveModel::ForbiddenAttributesProtection
ActiveRecord::Batches
ActiveRecord::Explain
ActiveRecord::Delegation
Enumerable
ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency
ActiveSupport::ToJsonWithActiveSupportEncoder
Object
JSON::Ext::Generator::GeneratorMethods::Object
ActiveSupport::Tryable
ActiveSupport::Dependencies::Loadable
Kernel
BasicObject

2. 祖先を調べる ```api.rubyonrails.org```で検索
https://api.rubyonrails.org/ を開き上から順番に検索

上から4つ目の、 ActiveRecord::Associations::CollectionProxy でヒット

3. ドキュメントを見る。
create, create! というメソッドが使えることがわかる。

ルーティング

ネストされたリソース

resources :staff_members do
  # このネストされたリソースが示すパスは
  # /staff_members/:staff_member_id/staff_events
  # ルーティング名は、
  # :staff_member_staff_events
  resources :staff_events, only: [:index]
end

kaminariを使ったページネーション

初期設定

  • ./bin/rails g kaminari:config
create config/initializers/kaminari_config.rb
  • ./bin/rails g kaminari:views default
create  app/views/kaminari/_gap.html.erb
create  app/views/kaminari/_next_page.html.erb
create  app/views/kaminari/_last_page.html.erb
create  app/views/kaminari/_page.html.erb
create  app/views/kaminari/_prev_page.html.erb
create  app/views/kaminari/_paginator.html.erb
create  app/views/kaminari/_first_page.html.erb
configファイルの変更
  • 初期値
# frozen_string_literal: true

Kaminari.configure do |config|
  # config.default_per_page = 25
  # config.max_per_page = nil
  # config.window = 4
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
  # config.max_pages = nil
  # config.params_on_first_page = false
end
  • 変更後
# frozen_string_literal: true

Kaminari.configure do |config|
  config.default_per_page = 25
  # config.max_per_page = nil
  # config.window = 4
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
  # config.max_pages = nil
  # config.params_on_first_page = false
end
erbテンプレートで使うラベルの日本語化
mkdir -p config/locales/views
touch config/locales/views/paginate.ja.yml
  • paginate.ja.yml の編集
ja:
  views:
    pagination:
      first: "先頭"
      last: "末尾"
      previous: "前"
      next: "次"
      truncate: "..."

使い方

  • コントローラのアクションでリストの範囲を絞り込む
    • kaminariのpageアクションを利用する。.page(nil)は、.page(1)と同じ。
@events = Event.order(created_at: :desc)
@events = @events.page(params[:page])
  • erbでpaginateの呼び出しをする。
<%= paginate @events %>

カスタマイズ

  • ページネーションのデザインを変更したい場合は、app/views/kaminari/以下のerbファイルを編集する。
  • app/views/kaminari/_gap.html.erb
  • app/views/kaminari/_next_page.html.erb
  • app/views/kaminari/_last_page.html.erb
  • app/views/kaminari/_page.html.erb
  • app/views/kaminari/_prev_page.html.erb
  • app/views/kaminari/_paginator.html.erb
    • ページネーションリンクの配置を決めている。
  • app/views/kaminari/_first_page.html.erb
    • 先頭リンクの表示を決めている。

N+1問題

  • railsの場合の都度select回避のメソッドは .includes()
@events = @events.includes(:member)

正規表現

  • 記号
    • 先頭・末尾に関するもの
      • 文字列の先頭
        • \A
      • 文字列の末尾
        • \z
      • 行頭
        • ^
      • 行末
        • $
    • その他
      • カタカナ1文字
        • \p{katakana}
      • 漢字1文字
        • \p{han}
      • ひらがな1文字
        • \p{hiragana}
      • アルファベット1文字
        • A-Za-z /\A[\u{30fc}]+\z/

黒田 努. Ruby on Rails 6 実践ガイド (impress top gearシリーズ) (Japanese Edition) (Kindle の位置No.8103). Kindle 版.

- 文字コードで1文字を指定(例: 長音符)
  - ```\u{30fc}```
  • 行頭・行末を指定したい場合
    • multiline: true が必要

バリデーション

  • オプション
    • presence: true
      • 値が空の場合に失敗
        • 半角スペース・タブ文字のみの場合も空という扱いになる。
    • format: { with: /\A[abc]+\z/, allow_blank: true}
      • 正規表現で判定、withには正規表現を、allow_blankがtrueの場合には空文字は判定をスキップする。
    • uniqueness DB上での一意性を確認
      • 例: uniqueness: true
      • 大文字小文字を区別しないようにする例: uniqueness: {case_sensitive: false}
    • confirmation
      • confirmation: true で _confirmationというsufixがついたキーと値が同一かを確認する。

組み込み以外のバリデーションを行う

  • validateブロックを使う
validate do
  unless foo_bar_validate?
    errors.add(:foo_bar, :wrong)
  end
end

バリデーションの前に処理を実行

  • コールバック(callbacks) / フック(hooks)
    • モデルオブジェクトに対してバリデーション、保存、削除などの操作が行われる前後に実行される処理
  • before_validationは、指定したブロックをコールバックとして登録するので、これを利用する。

nkfモジュール

  • 標準ライブラリ
  • NKF.nkf()
    • 第1引数: フラグ文字列。-WwZ1のように連結して利用も可。
    • 第2引数: 変換対象の文字列
    • return: 変換後の文字列
  • フラグ文字列
    • -W: 入力の文字コードをUTF8と仮定する
    • -w: UTF8で出力する
    • -Z1: 全角の英数字、記号、全角スペースを半角に変換する
    • --katakana: ひらがなをカタカナに変換する

require 'nkf'

module StringNormalizer
  extend ActiveSupport::Concern

  def normalize_as_name(text)
    NKF.nkf("-W -w -Z1", text).strip if text
  end

  def normalize_as_furigana(text)
    NKF.nkf("-W -w -Z1 --katakana", text).strip if text
  end

end

ブロックとprocとlambda

date_validatorモジュール

  • gemパッケージ

使用方

  • dateオプション
    • after: 指定された日付よりも後
    • before: 指定された日付よりも前
    • after_or_equal_to: 指定された日付よりも後で、指定された日付も含む
    • before_or_equal_to: 指定された日付よりも前で、指定された日付も含む
    • allow_blank: trueで空欄を許可
  • 注意
    • beforeなどに、日付を入れる場合はproc (-> (obj) {})で渡す必要がある。
      • production モードでは、起動時に1度クラスが読み込まれるが、before: 1.year.from_now.to_date としてしまうと、「起動時の時刻から1年後」と固定されてしまう。

利用例

  validates :start_date, presence: true, date: {
      after_or_equal_to: Date.new(2000, 1, 1),
      before: -> (obj) { 1.year.from_now.to_date },
      allow_blank: true
  }
  validates :end_date, date: {
      after: :start_date,
      before: -> (obj) { 1.year.from_now.to_date },
      allow_blank: true
  }

valid_email2モジュール

  • gemパッケージ

利用例

validates :email, presence: true, "valid_email_2/email": true

プレゼンター

erbのself

  • erbでselfと記載した場合のオブジェクトは、ビューコンテキストと呼ぶ
    • ビューコンテキスト
      • すべてのヘルパーメソッドを自分のメソッドとして持つオブジェクト

モデルプレゼンター

mkdir -p app/presenters
touch app/presenters/model_presenter.rb
  • app/presenters/model_presenter.rb
class ModelPresenter
  delegate :raw, to: :view_context
  attr_reader :object, :view_context

  def initialize(object, view_context)
    @object = object
    @view_context = view_context
  end
end
  • app/presenters/staff_member_presenter.rb
class StaffMemberPresenter < ModelPresenter
  delegate :suspended?, to: :object

  def suspended_mark
    suspended? ? raw("&#x2611;") : raw("&#x2610;")
  end
end
利用例
  • before
<% @staff_members.each do |m| %>
  <tr>
    <td><%= m.family_name %> <%= m.given_name %></td>
    <td><%= m.family_name_kana %> <%= m.given_name_kana %></td>
    <td class="email"><%= m.email %></td>
    <td class="date"><%= m.start_date.strftime("%Y/%m/%d") %></td>
    <td class="date"><%= m.end_date.try(:strftime, "%Y/%m/%d") %></td>
    <td class="boolean"><%= m.suspended? ? raw("&#x2611;") : raw("&#x2610;")%></td>
    <td class="actions">
      <%= link_to "編集", [ :edit, :admin, m] %> |
      <%= link_to "Events", [:admin, m, :staff_events] %> |
      <%= link_to "削除", [ :admin, m], method: :delete, data: { confirm: "本当に削除しますか?"} %>
    </td>
  </tr>
<% end %>
  • after
<% @staff_members.each do |m| %>
  <% p = StaffMemberPresenter.new(m, self) %>
  <tr>
    <td><%= m.family_name %> <%= m.given_name %></td>
    <td><%= m.family_name_kana %> <%= m.given_name_kana %></td>
    <td class="email"><%= m.email %></td>
    <td class="date"><%= m.start_date.strftime("%Y/%m/%d") %></td>
    <td class="date"><%= m.end_date.try(:strftime, "%Y/%m/%d") %></td>
    <td class="boolean"><%= p.suspended_mark %></td>
    <td class="actions">
      <%= link_to "編集", [ :edit, :admin, m] %> |
      <%= link_to "Events", [:admin, m, :staff_events] %> |
      <%= link_to "削除", [ :admin, m], method: :delete, data: { confirm: "本当に削除しますか?"} %>
    </td>
  </tr>
<% end %>

rubyの機能(api)

  • instance_variable_get
    • object.instance_variable_get(:@foo) とするとobjectが持つ@fooオブジェクトを取得できる。
  • シンボルに対しての&
    • &:fooは、{|e| e.foo } と同じ、つまり、map(&:foo)map {|e| e.foo }は同じ。

extend include prepend

railsの機能

delegate (委譲)

class Foo
   delegate :raw, :low to: bar

   def example
      # この呼び出しは、delegateにより、bar.raw という呼び出しとなる
      raw
      # この呼び出しは、delegateにより、bar.low という呼び出しとなる
      low
   end
end

with_options

  • 次の2つは同じ
m << p.foo(size: 32, required: true)
p.with_options(required: true) do |q|
  m << q.foo(size: 32)
end
  • with_optionsで指定された値はデフォルト値として振る舞う。 同じキーが指定されている場合、ブロック内での指定が優先される。(次の2つは同じ)
m << p.foo(size: 32, required: false)
p.with_options(required: true) do |q|
  m << q.foo(size: 32, required: false)
end

HtmlBuilder

mkdir -p app/lib
  • app/lib/html_builder.rb
module HtmlBuilder
  def markup(tag_name = nil, options = {})
    root = Nokogiri::HTML::DocumentFragment.parse("")
    Nokogiri::HTML::Builder.with(root) do |doc|
      if tag_name
        # 内部的にmethod_missingを使って実装されているため、次の2つは同じ
        # - doc.span "*", class: "mark"
        # - doc.method_missing("span", "*", class: "mark")
        doc.method_missing(tag_name, options) do
          yield(doc)
        end
      else
        yield(doc)
      end
    end
    root.to_html.html_safe
  end
end

使い方

  • <spanclass="mark">*</span>印の付いた項目は入力必須です。
markup do |m|
  m.span "*", class: "mark"
  m.text "印の付いた項目は入力必須です。"
end
  • <divclass="notes"><spanclass="mark">*</span>印の付いた項目は入力必須です。</div>
markup do |m| 
  m.div(class:"notes") do 
    m.span "*", class: "mark"
    m.text "印の付いた項目は入力必須です。"
  end
end
markup(:div,class:"notes") do |m| 
  m.span "*", class: "mark"
  m.text "印の付いた項目は入力必須です。"
end
  • ネスト例
markup do |m|
  m.div(id:"message") do
    m.div(class:"box") do 
      m.span"まもなくシステムが停止します。", class: "warning"
    end
  end
end
  • 追記例
markup do |m|
  m << "<spanclass='mark'>*</span>"
  m.text "印の付いた項目は入力必須です。"
end

利用例

class StaffEventPresenter < ModelPresenter
  delegate :member, :description, :occurred_at, to: :object

  def table_row
    markup(:tr) do |m|
      unless view_context.instance_variable_get(:@staff_member)
        m.td do
          m << link_to(member.family_name + member.given_name,
                       [ :admin, member, :staff_events ])
        end
      end
      m.td description
      m.td(:class => "date") do
        m.text occurred_at.strftime("%Y/%m/%d %H:%M:%S")
      end
    end
  end
end




<% @events.each do |event| %>
  <%= StaffEventPresenter.new(event, self).table_row %>
<% end %>

formプレゼンター

class FormPresenter
  include HtmlBuilder

  attr_reader :form_builder, :view_context
  delegate :label, :text_field, :date_field, :password_field,
           :check_box, :radio_button, :text_area, :object, to: :form_builder

  def initialize(form_builder, view_context)
    @form_builder = form_builder
    @view_context = view_context
  end

  def notes
    markup(:div, class: "notes") do |m|
      m.span "*", class: "mark"
      m.text "印の付いた項目は入力必須です。"
    end
  end

  def text_field_block(name, label_text, options = {})
    markup(:div, class: "input-block") do |m|
      m << decorated_label(name, label_text, options)
      m << text_field(name, options)
      m << error_messages_for(name)
    end
  end

  def password_field_block(name, label_text, options = {})
    markup(:div, class: "input-block") do |m|
      m << decorated_label(name, label_text, options)
      m << password_field(name, options)
      m << error_messages_for(name)
    end
  end

  def date_field_block(name, label_text, options = {})
    markup(:div, class: "input-block") do |m|
      m << decorated_label(name, label_text, options)
      m << date_field(name, options)
      m << error_messages_for(name)
    end
  end

  def error_messages_for(name)
    markup do |m|
      object.errors.full_messages_for(name).each do |message|
        m.div(class: "error-message") do |m|
          m.text message
        end
      end
    end
  end

  private def decorated_label(name, label_text, options = {})
    label(name, label_text, class: options[:required] ? "required" : nil)
  end
end
class StaffMemberFormPresenter < FormPresenter
  def password_field_block(name, label_text, options = {})
    if object.new_record?
      super(name, label_text, options)
    end
  end

  def full_name_block(name1, name2, label_text, options = {})
    markup(:div, class: "input-block") do |m|
      m << decorated_label(name1, label_text, options)
      m << text_field(name1, options)
      m << text_field(name2, options)
      m << error_messages_for(name1)
      m << error_messages_for(name2)
    end
  end

  def suspended_check_box
    markup(:div, class: "check-boxes") do |m|
      m << check_box(:suspended)
      m << label(:suspended, "アカウント停止")
    end
  end
end
<% p = StaffMemberFormPresenter.new(f, self) %>
<%= p.notes %>
<%= p.text_field_block(:email, "メールアドレス", size: 32, required: true) %>
<%= p.password_field_block(:password, "パスワード", size: 32, required: true) %>
<%= p.full_name_block(:family_name, :given_name, "氏名", required: true) %>
<%= p.full_name_block(:family_name_kana, :given_name_kana, "フリガナ", required: true) %>
<%= p.date_field_block(:start_date, "入社日", required: true) %>
<%= p.date_field_block(:end_date, "退職日") %>
<%= p.suspended_check_box %>

多言語化

  • 標準メッセージは、英語で用意されているが、gemパッケージ rails-i18n を導入すれば日本語も提供される。
  • エラーメッセージのカスタマイズは、config/localesディレクトリ以下にyaml形式で翻訳ファイルを設置する。
    • config/locales 直下でも、config/localesのサブディレクトリ直下でもどちらでもよい。

テーブル・リレーション

単一テーブル継承(STI)とポリモーフィック関連

単一テーブル継承(STI)

モデル

  • Image モデル
    • UserImage モデル
    • CustomerImage モデル
  • (Users モデル)・・・UserImage モデルを参照
  • (Companies モデル)・・・CustomerImage モデルを参照

テーブル

  • Image テーブル
    • id
    • type
      • UserImageかCustomerImageなのかのtype
    • title
    • url
  • (Users テーブル)
    • id
    • ImageのidをUserImage モデルのidとして持つ
  • (Companies テーブル)
    • id
    • ImageのidをCustomerImage モデルのidとして持つ
ポリモーフィック関連

モデル

  • Image モデル
  • (Users モデル)・・・Image モデルをimageableとして参照
  • (Companies モデル)・・・Image モデルをimageableとして参照

テーブル

  • Image テーブル
    • id
    • imageable_id
      • UsersかCompaniesテーブルのid
    • imageable_type
      • UsersかCompaniesなのかのtype
    • title
    • url
  • (Users テーブル)
    • id
  • (Companies テーブル)
    • id
単一テーブル継承(STI)の例
class CreateCustomers < ActiveRecord::Migration[6.0]
  def change
    create_table :customers do |t|
      t.string :email, null: false # メールアドレス
      t.string :family_name, null: false # 姓
      t.string :given_name, null: false # 名
      t.string :family_name_kana, null: false # 姓(セイ)
      t.string :given_name_kana, null: false # 名(メイ)
      t.string :gender # 性別
      t.date :birthday # 誕生日
      t.string :hashed_password # パスワード

      t.timestamps
    end

    add_index :customers, "LOWER(email)", unique: true
    add_index :customers, [ :family_name_kana, :given_name_kana ]
  end
end

class CreateAddresses < ActiveRecord::Migration[6.0]
  def change
    create_table :addresses do |t|
      t.references :customer, null: false # 顧客への外部キー
      t.string :type, null: false # 継承カラム
      t.string :postal_code, null: false # 郵便番号
      t.string :prefecture, null: false # 都道府県
      t.string :city, null: false # 市区町村
      t.string :address1, null: false # 町域、番地等
      t.string :address2, null: false # 建物名、部屋番号等
      t.string :company_name, null: false, default: "" # 会社名
      t.string :division_name, null: false, default: "" # 部署名

      t.timestamps
    end

    add_index :addresses, [ :type, :customer_id ], unique: true
    add_foreign_key :addresses, :customers
  end
end
class Customer < ApplicationRecord
  has_one :home_address, dependent: :destroy
  has_one :work_address, dependent: :destroy

  def password=(raw_password)
    if raw_password.kind_of?(String)
      self.hashed_password = BCrypt::Password.create(raw_password)
    elsif raw_password.nil?
      self.hashed_password = nil
    end
  end
end

class Address < ApplicationRecord
  belongs_to :customer

  PREFECTURE_NAMES = %w(
    北海道
    青森県 岩手県 宮城県 秋田県 山形県 福島県
    茨城県 栃木県 群馬県 埼玉県 千葉県 東京都 神奈川県
    新潟県 富山県 石川県 福井県 山梨県 長野県 岐阜県 静岡県 愛知県
    三重県 滋賀県 京都府 大阪府 兵庫県 奈良県 和歌山県
    鳥取県 島根県 岡山県 広島県 山口県
    徳島県 香川県 愛媛県 高知県
    福岡県 佐賀県 長崎県 熊本県 大分県 宮崎県 鹿児島県
    沖縄県
    日本国外
  )
end
class HomeAddress < Address
end
class WorkAddress< Address
end

メソッド

  • build_foo_bar
    • オブジェクトが、has_one関係を持っている場合、例えば hasone :foo_barであれば、object.build_foo_bar で初期状態のインスタンスを作成して紐付ける事ができる。(DBには保存されない。)

Capybara

概要

  • UIテストライブラリ

準備(option)

Capybaraがデフォルトで使用するホスト名は、 www.example.com のため、変更する場合は、Capybara.app_hostを使う。

  • spec/support/features_spec_helper.rb
module FeaturesSpecHelper
  def switch_namespace(namespace)
    config = Rails.application.config.myapp
    Capybara.app_host = "http://" + config[namespace][:host]
  end

  def login_as_staff_member(staff_member, password = "pw")
    visit staff_login_path
    within("#login-form") do
      # fill_inには、「ラベル文字列」か「input要素のid属性かname属性の値」が指定できる。
      fill_in "メールアドレス", with: staff_member.email
      fill_in "パスワード", with: password
      # click_buttonには、「ラベル文字列」か「input要素かbutton要素のid属性の値」が指定できる。
      click_button "ログイン"
    end
  end
end

ルール

  • capybaraを利用したspecファイルは原則、spec/featuresディレクトリ以下に配置する。
  • describe/example の代わりにfeature/scenarioというエイリアスが用意されているので、こちらを利用する。

3セクション

  • senarioテストは3セクション構成で記述するのが一般的
    • 1.Gevenセクション
      • シナリオの前提条件
    • 2,Whenセクション
      • シナリオ本体:テスト対象となるブラウザ上の操作
    • 3,Thenセクション
      • 期待する結果を確かめる