【復習】Railsのflashの使用方法

Railsではviewで一時的にテキストを表示するするために、flashというハッシュ形式のオブジェクトが設定されている。

flashkeyにはデフォルトでnoticealertが設定されているが、add_flash_typeskeyを追加できる。

add_flash_types :info, :warning  # %i(info warning)のように、%記法は使えない?

add_flash_typesを記入することでflashメッセージを1行で記載できる

# before
flash[:info] = 'hoge'
redirect_to root_url

redirect_to root_url, flash: { info: 'hoge' }  #上と同じ

# after
redirect_to root_url, info: 'hoge'

redirect_toでは指定したpathにルーティング⇨コントローラ⇨viewと画面を遷移させているので、さらに次のviewではflashメッセージは表示されない。対してrenderではaction名に対応したviewを表示させているので、さらに次のviewでもflashメッセージは表示される。
 ⇨renderではflash.now[:info]とすることで現在の表示に対してflashを行う

flash.now[:info] = 'hoge'
render action: :new

【復習】Railsの多言語化に関する設定

Railsではさまざまな言語に対応するためにi18nというgemが元々導入されている。

・以下、英語化の対応方法

1.デフォルトの言語を英語にする & 日本語化するためのファイルのpathを通す

# config.application

config.i18n.default_locale = :en  # デフォルトの言語設定 
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '*', '.{rb,yml}').to_s]  # pathを通す

2.翻訳するファイルにlazy lookup記法で翻訳名を記入する

en:
  activerecord:
    models:
      user: Customer
    attributes:
      user:
        login: Handle
  books:
    index:
      title: Título

3.lazy lookup記法でactiverecord:以下にmodel:attributes:がある場合、Model.model_name.humanメソッドとModel.human_attribute_name(attribute)メソッドを使用することで対応した訳文を返す。

User.human_attrubute_name(:login)  # Handleを返す

補足:form_with model: @userのように、form_withmodelオプションにUserモデルのインスタンスを渡すと、Railsが自動で翻訳してくれる。 modelオプションがない場合は3. に書いたように明示する必要がある。

【復習】sorceryのメソッドまとめ、及びログインとは

sorceryについて

sorceryとは:Railsで認証機能を実装してくれるメソッドをまとめてくれたgem

・暗号化されたパスワード(crypted_pasword)とsaltカラムのデータはユーザーに直接閲覧や編集をさせたくないから、viewで使用できない。  ⇨crypted_paswordに対応する仮想的な属性(password, password_confirmation)をviewに使用する
 ⇨password属性はカラムに対応してないので、入力された情報がデータベースには保存されない

loginメソッド: login(user, password)のように引数を2つとり、引数の情報と合致するデータベースのユーザーをセッション状態にする。(第一引数はemailといったユーザーを特定するもの、第二引数はpasswordといったcrypted_paswordに対応する属性?)

logoutメソッド: ユーザーのセッション状態を解除する。

require_loginメソッド: ログイン状態を要求するメソッド。検証用(before_action)に使用する。

not_authenticatedメソッド: ログインが認証されなかった際にroot_pathに遷移するメソッド。上書き可能。

logged_in?メソッド: ログイン状態かを検証するメソッド。内部でcurrent_userメソッドが使用されている。

redirect_back_or_toメソッド: フレンドリーフォワーディング(ログインが必要なページ(ページA)に未ログイン状態でアクセスした場合に、ログインページに遷移させてログイン後にページAに戻す機能)を実装するためのメソッド

セッションとCookieについて

Cookieとは:ブラウザ側で保存される小さな情報、または領域

セッションとは:サーバー側で一時的に保存される小さな情報、または領域

ステートレスは:前後の繋がりで状態を保持してないこと。HTTPリクエストがこれに当たる

ログインとは:ユーザーの情報をセッションやCookieに保存させておいて、異なるHTTPリクエストに繋がりを持たせること

form_withについて

form_withとはform_tagform_for両方の特徴を兼ね備えたフォーム生成ヘルパー

・モデルに紐づくフォームを生成する時

form_with model: モデルから生成されたインスタンス(@object) do |f|
  フォーム内容
end

モデルから生成されたインスタンスに対して、persisted?メソッドの返り値がfalseの場合createアクションに、trueならupdateアクションに送る。その時、インスタンスの生成元のモデルと同名のコントローラのアクションに自動で遷移するようにパスを生成してくれる。

・モデルに紐づかないフォームを生成する時(create, updateを行うコントローラとモデルが紐づかない場合も)

form_with url: "パス名"(login_path), method: :メソッド名'(get) do |f|
  フォーム内容
end

【復習】RubyでのJavaScriptファイルの読み込み方

・ブラウザに出力されるHTMLファイルを生成する時、「layoutファイル+個別ファイル(テンプレートファイル)」の2つのファイル を足し合わせてHTMLを生成する

・Sprocketsを使用する場合、CSSを読み込むにはstylesheet_link_tag、JSファイルの読み込みはjavascript_include_tagというヘルパーメソッドを使用し、第一引数に読み込みたいアセットファイルを指定する

stylesheet_link_tag    'application'
javascript_include_tag 'application'

・Webpackerを使用する場合、CSSを読み込むにはstylesheet_pack_tag、JSファイルの読み込みはjavascript_pack_tagというヘルパーメソッドを使用し、第一引数に読み込みたいアセットファイルを指定する

stylesheet_pack_tag    'application'
javascript_pack_tag    'application'

・デフォルトで指定されているapplication.css、application.jsはマニュフェストファイルと呼ばれ、どのファイルをどのように連結して出力するかをまとめるファイルである

・マニュフェストファイルに含むファイルの指定には、ディレクティブと呼ばれる記法で指定する必要がある。以下CSSとJSのディレクティブ

# JavaScriptのディレクティブ
//= require ファイル名  # 指定したJavaScriptファイルの内容を取り込む
//= require_tree     # 指定したディレクトリは以下の全ファイルを読み込む
//= require_self     # 地震のファイルを読み込む

CSSのディレクティブ
//*= require ファイル名  # 指定したJavaScriptファイルの内容を取り込む

Sassでディレクティブ(CSSの//*= も使えるっぽい?)
@import "ファイル名";

・image_tagを含んだlink_toヘルパーの書き方
link_to image_tag("/assets/imageにある画像のファイル名"), パス名

以下の書き方でも同じ挙動

link_to パス名 do
  image_tag "画像のファイル名"
end

Railsの雑多なメモ(assign_attributes、バリデーションnumericality、render_to_string、delegate、truncate)

assign_attributes

特定のattributeを変更するためのメソッド。オブジェクトの変更をしただけで、DBには保存されない。

article.assign_attributes( article_params )

def article_params
  params.require(:article).permit( :title, :description, :state )
end

Active Recordのattributesの更新メソッド | 酒と涙とRubyとRailsと

ActiveRecord の attribute 更新方法まとめ - Qiita

バリデーションnumericality

属性に数値のみ(Numericクラス)が使われていることを検証するバリデーション。

class Player < ApplicationRecord
  validates :points, numericality: true
end

数値の上限を制限するless_than_or_equal_toオプションや、下限を制限するgreater_than_or_equal_toオプションなど、オプション指定することで制約を追加できる。

class Player < ApplicationRecord
  # 100以下の数値になるように制限する
  validates :points, numericality: { less_than_or_equal_to: 100 }

  # 10以上の数値になるように制限する
  validates :points, numericality: { less_than_or_equal_to: 10 }

  # 以下のように複数オプションを指定することもできる
  validates :points, numericality: { less_than_or_equal_to: 100, greater_than_or_equal_to: 10 }
end

render_to_string

コントローラーないで使用するメソッド。renderした結果のviewをブラウザで表示する必要がなく、文字列として取得する場合はrender_to_stringメソッドを使用する。

# render先のviewを文字列で返している
controller.render_to_string("shared/_media_image", locals: { medium: medium }, layout: false)

=> "<div class=\"media-image\"><img src=\"http://localhost:3000/rails/active_storage/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBRQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--dde48d8f52e682864a7f2b7d68a3468f7f1fa227/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9MY21WemFYcGxTU0lOTVRBeU5IZzNOamdHT2daRlZBPT0iLCJleHAiOm51bGwsInB1ciI6InZhcmlhdGlvbiJ9fQ==--6ec66a681693b203f182cf67cd6ee51b8eb65eca/sample01.jpg\" /></div>"

レイアウトとレンダリング - Railsガイド

delegate

アソシエーションを組んだモデルのメソッドを簡単に移譲できるようにするメソッド。
以下の記述をすることで、user.profile.nameを記述する所をuser.nameと記述することができる。

class User
  has_one :profile
  delegate :name, to: :profile
end

prefixオプションをtrueにすると、生成されるメソッド名にプレフィックスを追加できる。今回の場合だとメソッド名がuser.profile_nameになる。

class User
  has_one :profile
  delegate :name, to: :profile, prefix: true, allow_nil: true
end

Active Support コア拡張機能 - Railsガイド

truncate

文字列の最後を切り捨てて省略表示するメソッド。String#truncateTextHelperとしてのtruncateがある。

'Once upon a time in a world far far away'.truncate(27)
# => "Once upon a time in a wo..."
truncate("Once upon a time in a world far far away", length: 20)
# => "Once upon a time ..."

Rails - 長い文字列を省略して表示する - Qiita

FactoryBotのtrait、transient、evaluatorについて

概要

Railsのテストでテストデータを作成する際に便利なgemである、FactoryBotにあるtrait、transient、evaluatorの機能について備忘録として書きます。

trait

通常のテストデータの作成とは別に、モデルの属性値の集合を定義できる。共通の属性値を持ったテストデータの作成時、重複を避けることができる。
一例として、以下のようなUserのファクトリを作成します。

FactoryBot.define do
  factory :user do
    sequence(:name) { |n| "admin-#{n}" }
    password { 'password' }
    password_confirmation { 'password' }
    role { :admin }

    trait :writer do
      sequence(:name) { |n| "writer-#{n}" }
      role { :writer }
    end
  end
end

通常の書き方は以下の通りで、nameがadmin-1、roleがadminのデータを作成している。

let(:user) { create(:user) }

pry(~)> user
 => #<User:0x00007fd7e6a9f0e0
 id: 1,
 name: "admin-1",
 password: "password",
 password_confirmation: "password",
 role: "admin",
 created_at: Tue, 25 May 2021 03:14:48 JST +09:00,
 updated_at: Tue, 25 May 2021 03:14:48 JST +09:00>

次にtraitを活用したuserは、nameがwriter-1、roleがwriterのデータを作成している。

let(:writer) { create(:user, :writer) }

pry(~)> writer
 => #<User:0x00007fd7f23b4eb8
 id: 2,
 name: "writer-1",
 password: "password",
 password_confirmation: "password",
 role: "writer",
 created_at: Tue, 25 May 2021 03:15:04 JST +09:00,
 updated_at: Tue, 25 May 2021 03:15:04 JST +09:00>

transient

テストデータの元となるモデルに定義されてない任意の属性を定義することができる。
一例として、以下のようなUserのファクトリを作成します。

FactoryBot.define do
  factory :user do
    transient do
      state_name { 'user' }
    end

    name { "tanaka(#{state_name})" }
    password { 'password' }
    password_confirmation { 'password' }

    trait :admin do
      state_name { 'admin' }
    end

    trait :writer do
      state_name { 'writer' }
    end
  end
end

traitのadminを使用した場合、nameがtanaka(admin)のデータを作成している。

let(:admin) { create(:user, :admin) }

pry(~)> admin.name
=> "tanaka(admin)"

traitのwriterを使用した場合、nameがtanaka(writer)のデータを作成している。

let(:writer) { create(:user, :writer) }

pry(~)> writer.name
=> "tanaka(writer)"

evaluator

FactoryBotのcallbackとして渡されている引数。その中身は、FactoryBotで作成されたインスタンスも、transientで定義した属性もハッシュの形で返してくれるオブジェクト。transientで定義した属性を取り出したいときに必要となる引数。
一例として、Articleを作成後、一対多のアソシエーションを結んだAuthorモデルのデータを作成するファクトリを作成します。

FactoryBot.define do
  factory :article do
    sequence(:title) { |n| "title-#{n}" }
    state { :published }
  end

  trait :with_author do
    transient do
      sequence(:author_name) { |n| "test_author_name_#{n}" }
    end

    after(:build) do |article, evaluator|
      article.author = build(:author, name: evaluator.author_name)
    end
  end

with_authorのtraitを使用したデータを作成後、authorメソッドを使用することでAuthorモデルのデータを取得することができる。

let(:article_with_author) { create(:article, :with_author) }

pry(~)> article_with_author.author
=> #<Author:0x00007f9a263a0670
 id: nil,
 name: "test_author_name_1",
 created_at: nil,
 updated_at: nil>

Punditを使用した認可機能の作成方法

前提

scaffoldで作成したようなスタンダードなRailsアプリを想定しており、UserとPostモデルがあり1対多の関係にあるものを想定しています。

class User < ApplicationRecord
  has_many :posts

  enum state: { writer: 0, editor: 1, admin: 2 }
end

class Post < ApplicationRecord
  belongs_to :user
end

基本的な使い方

1. punditをインストールした後、application_controller.rbにPunditを適用させる
# Gemfile

gem 'pundit'
=> bundle installコマンドを実行

application_controller.rbにPunditを適用させる

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Pundit
end
2. 以下のコマンドを実行後、生成されたファイルに設定を記述する.

以下のコマンドを実行することで、app/policies/配下にapplication_policy.rbというファイルが生成される。

% bin/rails g pundit:install

application_policy.rbに以下の設定を記述する

# app/policies/application_policy.rb

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end
end

initializeメソッドでインスタンス変数を定義している。
第一引数はuserを渡してある。Punditはデフォルトでコントローラーでcurrent_userメソッドを呼んで、userとして自動的に第一引数のuserに渡してある。
2番目の引数は、認証をチェックしたい何らかのモデルオブジェクトです。

3. コントローラーの各アクションの実行前にauthorizeを実行する.
# # app/controllers/articles_controller.rb

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  before_action :authorize_post, only: [:index, :new, :create]

  def index; end

  def show; end

  def new; end

  def create; end

  def edit; end

  def update; end

  def destroy; end

  private

  def set_post
    @post = Post.find(params[:id])
    authorize @post
  end

  def authorize_post
    authorize Post
  end
end
4. 個別のPolicyファイルを作成し、設定を記述する.

Punditは、リソースに対して、どのユーザーであれば処理が許可されるのかを定義するものです。
「コントローラのアクション名 + ?」のメソッドを定義し、返り値が true なら許可、 false なら拒否になる。例えばPostモデルのオブジェクトに対して以下の設定を行う場合、

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  def index?
    true
  end

  def show?
    true
  end

  def new?
    true
  end

  def create?
    user.admin? || user.editor?
  end

  def edit?
    true
  end

  def update?
    user.admin? || user.editor?
  end

  def destroy?
    user.admin? || user.editor?
  end
end

上記のようにポリシーファイルを設定すると、Postモデルのオブジェクトに対して、create、update、destoryメソッドは管理者、編集者のみが実行可能、他のアクションは全てのユーザーが実行可能、といった定義を行うことが出来る。

Scope

policy_scopeメソッドを呼ぶと、対象ポリシーファイル内で定義されたスコープを実行してくれる。
例えば、管理者はすべてのリソースにアクセスできるが、一般ユーザは自身が保持しているリソースのみといった認可を設定したい場合、以下のように実装すれば良い。

class PostPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if user.admin?
        scope.all
      else
        scope.where(user_id: user.id)
      end
    end
  end
end
class PostsController < ApplicationController
  before_action :authorize_post, only: [:index]

  def index
    @posts = policy_scope(Post)
  end

  private

  def authorize_post
    authorize policy_scope(Post)
  end
end

例外を403ステータスにするする方法

# config/application.rb

module Post
  config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
end

補足:Punditが用意してあるPolicySpecでテストコードを書く

spec_helper.rbに以下のコードを記述

# spec/spec_helper.rb

require "pundit/rspec"

上記の記述によりpermitというヘルパーが使用できるようになるので、これにユーザとリソースオブジェクトを渡すだけで書ける。

require 'rails_helper'

RSpec.describe TaxonomyPolicy do
  subject { described_class }

  permissions :index?, :create?, :update?, :destroy? do
    let(:admin) { create(:user, :admin) }
    let(:editor) { create(:user, :editor) }
    let(:writer) { create(:user, :writer) }
    let(:tag) { create(:tag) }
    let(:author) { create(:author) }
    let(:category) { create(:category) }

    it "ライターはタグ・著者・カテゴリーの一覧表示・編集・削除機能にアクセスできない" do
      expect(subject).not_to permit(writer, tag, author, category)
    end

    it "管理者はタグ・著者・カテゴリーの一覧表示・編集・削除機能にアクセスできない" do
      expect(subject).to permit(admin, tag, author, category)
    end

    it "編集者はタグ・著者・カテゴリーの一覧表示・編集・削除機能にアクセスできない" do
      expect(subject).to permit(editor, tag, author, category)
    end
  end
end

公式リファレンス・参考記事

https://github.com/varvet/pundit

https://qiita.com/zaru/items/8bf7b41b33f3f55bd27d#%E3%83%86%E3%82%B9%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E6%9B%B8%E3%81%8F