ActiveRecordのpluckメソッドでjoinsした先のカラムの値を取得したい

こんにちは!kossyです!




さて、今回はActiveRecordのpluckメソッドでjoinsした先のカラムの値を取得する方法について、ブログに残してみたいと思います。



環境

Ruby 2.6.8
Rails 6.0.4
MacOS catalina



前提

以下のテーブル構成があるとします。

class CreateCompanies < ActiveRecord::Migration[6.0]
  def change
    create_table :companies do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end
class CreatePositions < ActiveRecord::Migration[6.0]
  def change
    create_table :positions do |t|
      t.string :name, null: false
      t.references :company, null: false, foreign_key: true, index: true

      t.timestamps
    end
  end
end
class CreateDepartments < ActiveRecord::Migration[6.0]
  def change
    create_table :departments do |t|
      t.string :name, null: false
      t.string :ancestry
      t.integer :ancestry_depth
      t.references :company, null: false, foreign_key: true, index: true

      t.timestamps
    end
  end
end
class CreateEmployees < ActiveRecord::Migration[6.0]
  def change
    create_table :employees do |t|
      t.string :first_name, null: false
      t.string :last_name, null: false
      t.string :first_name_kana, null: false
      t.string :last_name_kana, null: false
      t.string :middle_name
      t.string :middle_name_kana
      t.boolean :sex, null: false
      t.date :hired_at, null: false
      t.date :retired_at
      t.references :company, null: false, foreign_key: true, index: true
      t.references :position, null: false, foreign_key: true, index: true
      t.references :department, null: false, foreign_key: true, index: true

      t.timestamps
    end
  end
end
class CreateOrders < ActiveRecord::Migration[6.0]
  def change
    create_table :orders do |t|
      t.string :customer_name, null: false
      t.integer :price, null: false
      t.integer :status, null: false
      t.date :ordered_at, null: false
      t.references :employee, null: false, foreign_key: true, index: true
      t.references :company, null: false, foreign_key: true, index: true

      t.timestamps
    end
  end
end
class Company < ApplicationRecord
  with_options dependent: :destroy do
    has_many :employees
    has_many :departments
    has_many :orders
    has_many :positions
  end
end
class Department < ApplicationRecord
  belongs_to :company
  has_many :employees
  has_ancestry cache_depth: true
end
class Employee < ApplicationRecord
  belongs_to :company
  belongs_to :department
  belongs_to :position
  has_many :orders

  enum sex: { male: false, female: true }
end
class Order < ApplicationRecord
  belongs_to :employee
  belongs_to :company

  enum status: [:un_official, :fixed]
end
class Position < ApplicationRecord
  belongs_to :company
  has_many :employees
end

pluckを使ってjoins先の値を取得

例えば、Employeeが所属する会社の名前を取得したいとします。

この場合、以下のように記述することで、目的の値を取得できます。

$ Employee.joins(:company).pluck('companies.name')

以下のようなjoins文を書いても、joins先のテーブルの値を取得することができます。

$ Company.joins(employees: :orders).distinct.pluck('orders.price')

勉強になりました。


大いに参考にさせていただいたサイト

素晴らしい記事の執筆誠にありがとうございます。

Railsアプリの処理を100倍以上に高速化して得られた知見 – PSYENCE:MEDIA

devise_token_authのregistrations_controllerのcreateアクションのソースコードを追ってみる

こんにちは!kossyです!



さて、今回はOSSソースコードリーディング回ということで、devise_token_authのregistrations_controllerのcreateアクションのソースコードを読んでみたので、ブログに残してみたいと思います。



コードリーディング

registrations_controllerのcreateアクションは、ユーザー登録を担うアクションになっています。

devise_token_auth/registrations_controller.rb at master · lynndylanhurley/devise_token_auth · GitHub

    def create
      build_resource

      unless @resource.present?
        raise DeviseTokenAuth::Errors::NoResourceDefinedError,
              "#{self.class.name} #build_resource does not define @resource,"\
              ' execution stopped.'
      end

      # give redirect value from params priority
      @redirect_url = params.fetch(
        :confirm_success_url,
        DeviseTokenAuth.default_confirm_success_url
      )

      # success redirect url is required
      if confirmable_enabled? && !@redirect_url
        return render_create_error_missing_confirm_success_url
      end

      # if whitelist is set, validate redirect_url against whitelist
      return render_create_error_redirect_url_not_allowed if blacklisted_redirect_url?(@redirect_url)

      # override email confirmation, must be sent manually from ctrl
      callback_name = defined?(ActiveRecord) && resource_class < ActiveRecord::Base ? :commit : :create
      resource_class.set_callback(callback_name, :after, :send_on_create_confirmation_instructions)
      resource_class.skip_callback(callback_name, :after, :send_on_create_confirmation_instructions)

      if @resource.respond_to? :skip_confirmation_notification!
        # Fix duplicate e-mails by disabling Devise confirmation e-mail
        @resource.skip_confirmation_notification!
      end

      if @resource.save
        yield @resource if block_given?

        unless @resource.confirmed?
          # user will require email authentication
          @resource.send_confirmation_instructions({
            client_config: params[:config_name],
            redirect_url: @redirect_url
          })
        end

        if active_for_authentication?
          # email auth has been bypassed, authenticate user
          @token = @resource.create_token
          @resource.save!
          update_auth_header
        end

        render_create_success
      else
        clean_up_passwords @resource
        render_create_error
      end
    end

before_actionが呼ばれるのでまずはそちらから読んでみます。

# https://github.com/lynndylanhurley/devise_token_auth/blob/4c5245b88b39c1bb305e0cbdbfc2513eebdeda93/app/controllers/devise_token_auth/registrations_controller.rb#L189


def validate_sign_up_params
  validate_post_data sign_up_params, I18n.t('errors.messages.validate_sign_up_params')
end

# https://github.com/lynndylanhurley/devise_token_auth/blob/4c5245b88b39c1bb305e0cbdbfc2513eebdeda93/app/controllers/devise_token_auth/registrations_controller.rb#L189

def validate_post_data which, message
  render_error(:unprocessable_entity, message, status: 'error') if which.empty?
end

# https://github.com/lynndylanhurley/devise_token_auth/blob/4c5245b88b39c1bb305e0cbdbfc2513eebdeda93/app/controllers/devise_token_auth/registrations_controller.rb#L91

def sign_up_params
  params.permit(*params_for_resource(:sign_up))
end

sign_up_paramsが空の時かどうかをチェックして、空の場合はエラーを返すバリデーションでした。

こういった、実際にcontroller内で値の処理を行う前にパラメータに対して何かしらのバリデーションを実行するのは定石ですね。

次にbuild_resourceメソッドを読んでみます。

devise_token_auth/registrations_controller.rb at master · lynndylanhurley/devise_token_auth · GitHub

def build_resource
  @resource            = resource_class.new(sign_up_params)
  @resource.provider   = provider

  # honor devise configuration for case_insensitive_keys
  if resource_class.case_insensitive_keys.include?(:email)
    @resource.email = sign_up_params[:email].try(:downcase)
  else
    @resource.email = sign_up_params[:email]
  end
end

resource_classはdeviseをincludeしているモデルのことですね。例えばUserモデルが定義されていた場合は、

@resource = User.new(sign_up_params)

となります。次の処理の「provider」はdefaultでemailになっています。

resource_class.case_insensitive_keys.include?(:email)はdeviseの定義が存在していればそちらを優先して処理を行います。

deviseの定義はconfig/initializers/devise.rbの、

  # Configure which authentication keys should be case-insensitive.
  # These keys will be downcased upon creating or modifying a user and when used
  # to authenticate or find a user. Default is :email.
  config.case_insensitive_keys = [:email]

の部分です。

case_insensitive_keysにemailが含まれている場合は、emailをdowncaseして、そうでない場合はパラメータで送信されたemailをそのままresourceのattrとして代入しています。

createアクションに戻ります。

unless @resource.present?
  raise DeviseTokenAuth::Errors::NoResourceDefinedError,
        "#{self.class.name} #build_resource does not define @resource,"\
        ' execution stopped.'
end

もしbuild_resourceを実行して、@resourceが存在しない場合は、例外を発生させています。

次の処理は、リダイレクト先のURLを定義しています。

その次のif文は、deviseをincludeしているモデルがconfirmableモジュールをincludeしていて、redirect_urlがnilの場合、「リダイレクト先のURLがない」というエラーを返却していました。

blacklisted_redirect_url?は内部の処理を見ましょう。

devise_token_auth/application_controller.rb at 4c5245b88b39c1bb305e0cbdbfc2513eebdeda93 · lynndylanhurley/devise_token_auth · GitHub

def blacklisted_redirect_url?(redirect_url)
  DeviseTokenAuth.redirect_whitelist && !DeviseTokenAuth::Url.whitelisted?(redirect_url)
end

devise_token_authのconfigファイルでホワイトリストを設定していて、かつ引数のredirect_urlがホワイトリストで設定されたURLでない場合はtrueを返すメソッドでした。

次の行を見てみます。

callback_name = defined?(ActiveRecord) && resource_class < ActiveRecord::Base ? :commit : :create
resource_class.set_callback(callback_name, :after, :send_on_create_confirmation_instructions)
resource_class.skip_callback(callback_name, :after, :send_on_create_confirmation_instructions)

ActiveRecordが定義されていて、かつresource_classがActiveRecordを継承していた場合、:commitが、そうでなければ :create がcallback_nameとして定義されていました。

その次の行はresource_classに対してcallback_nameのcallbackとskip_callbackをセットしていました。

次の行は、実質的にresource_classがconfirmableモジュールをincludeしているかどうかを判定しています。

devise/confirmable.rb at master · heartcombo/devise · GitHub

疲れてきたので最後はソースコードに適宜コメントを入れて書いていきます、、、

      # saveに成功した場合
      if @resource.save

        # ブロックが渡されていた場合は渡されたブロック内で@resourceを使って処理
        yield @resource if block_given?

        # 確認済みでない場合(confired_atに値がない場合)
        unless @resource.confirmed?
          # user will require email authentication
          @resource.send_confirmation_instructions({
            client_config: params[:config_name],
            redirect_url: @redirect_url
          })
        end

        # resource_classがdatabase_authenticatableをincludeしている場合
        if active_for_authentication?
          # email auth has been bypassed, authenticate user
          @token = @resource.create_token
          @resource.save!
          update_auth_header
        end

        render_create_success
      else
        clean_up_passwords @resource
        render_create_error
      end

これでひと通り目を通せました。

おわりに

confirmableモジュールを使っている場合の条件分岐がそこそこあったので、ユーザー登録後の確認メール機能を実装する場合はこのブログが役に立ってくれること願っています。

特定のレコードの前後のレコードを取得する LAGとLEAD関数を試す

こんにちは!kossyです!


今回は、特定のレコードの前後のレコードを取得する LAGとLEAD関数を試してみたので、
備忘録としてブログに残してみたいと思います。




環境

MySQL 8系


LAG・LEAD関数とは?

まずはドキュメントを見てみましょう。

www.postgresql.jp

LAG

パーティション内の現在行よりoffset行だけ前の行で評価されたvalueを返す。 該当する行がない場合、その代わりとしてdefault(valueと同じ型でなければならない)を返す。 offsetとdefaultは共に現在行について評価される。 省略された場合、offsetは1となり、defaultはNULLになる。

出典: 9.21. ウィンドウ関数

LEAD

パーティション内の現在行よりoffset行だけ後の行で評価されたvalueを返す。 該当する行がない場合、その代わりとしてdefault(valueと同じ型でなければならない)を返す。 offsetとdefaultは共に現在行について評価される。 省略された場合、offsetは1となり、defaultはNULLになる。

出典: 9.21. ウィンドウ関数

「現在行よりoffset行だけ前/後の行」が肝ですね。次の項で実際にSQLを実行してみます。



実際に試してみる

www.db-fiddle.com

上記のサイトでLAGとLEAD関数を試すことができますので活用します。

Schema SQLの部分に以下を入力します。

CREATE TABLE users (
  id INT
);

CREATE TABLE scores (
  id INT,
  math INT,
  exam_date DATE,
  user_id INT
);

INSERT INTO users (id) VALUES (1);

INSERT INTO scores (id, math, exam_date, user_id) VALUES (1, 100, '2021-07-21', 1);
INSERT INTO scores (id, math, exam_date, user_id) VALUES (2, 80, '2021-06-21', 1);
INSERT INTO scores (id, math, exam_date, user_id) VALUES (3, 90, '2021-05-21', 1);
INSERT INTO scores (id, math, exam_date, user_id) VALUES (4, 75, '2021-04-21', 1);
INSERT INTO scores (id, math, exam_date, user_id) VALUES (5, 85, '2021-03-21', 1);

生徒テーブルと試験結果テーブルを作成しています。科目は一旦数学だけです。

次に、作成したテーブルに対して実行するSQLを書いてみます。まずはLAGです。

select id, exam_date, math, lag(math) over(order by exam_date desc) as next_math_score
from scores
where scores.user_id = 1
order by exam_date desc;

実行結果は以下です。

f:id:kossy-web-engineer:20210722151320p:plain

lag(math) over(order by exam_date desc) as next_math_score と select文に記載することで、
次の行の値を列として出力することができます。

次に、LEAD関数も試してみましょう。

select id, exam_date, math, lead(math) over(order by exam_date desc) as previous_math_score
from scores
where scores.user_id = 1
order by exam_date desc;

実行結果は以下です。

f:id:kossy-web-engineer:20210722151835p:plain

LEAD関数の場合は、前の行の値を列として出力されています。

おわりに

LAGとLEAD関数は、前回の結果や今回の結果を一緒に出力したい場合に使えるウィンドウ関数かと思います。

他のウィンドウ関数(SUMやMIN、MAX)と比べると使う機会は少ないかもしれませんが、覚えておいて損はないでしょう。


SQLのWITH RECURSIVEの使い方とユースケース

こんにちは!kossyです!



今回はSQLのWITH RECURSIVEの使い方とユースケースを考えてみたので、備忘録としてブログに残してみたいと思います。



環境

PostgreSQL 12系



使い方

公式のドキュメントを見てみましょう。

www.postgresql.jp

オプションのRECURSIVE修飾子は、WITHを、単に構文上の利便性の高めるだけでなく標準的なSQLでは不可能な機能を実現させます。 RECURSIVEを使用すれば、WITH問い合わせが行った自己の結果を参照できるようになります。1から100までの数を合計する非常に単純な問い合わせは以下のようなものです。


WITH RECURSIVE t(n) AS (
VALUES (1)
UNION ALL
SELECT n+1 FROM t WHERE n < 100
)
SELECT sum(n) FROM t;

再帰的WITH問い合わせの汎用形式は常に、非再帰的表現(non-recursiveterm)、そしてUNION(またはUNION ALL)、そして再帰的表現(recursive term)です。 再帰的表現だけが、その問い合わせ自身の出力への参照を含むことができます。

例文をSQLクライアントで実行してみます。

f:id:kossy-web-engineer:20210719220339p:plain

繰り返し処理された結果を得られています。

ドキュメントを読み進めると、

厳密には、この手順は反復(iteration)であって再帰(recursion)ではありませんが、RECURSIVEはSQL標準化委員会で選ばれた用語です。

という一文が出てきます。反復処理だということを頭に入れておくと、多少は理解が進むのではないかと思います。

ユースケース

例えば任意の範囲の日付を取得したい場合ですと、以下のように書くことができます。

with recursive calculate_range(i, date_start, date_end) AS (
  select date '2021-01-01', date '2022-01-31'
  union all
  select date (date_start + interval '1 month'), date (date_start + interval '2 month' - interval '1 day')
  from calculate_range
  where date_start < '2022-01-01'
)
select calculate_range.*
from calculate_range

このSQLを実行すると、以下のような結果が得られます。

f:id:kossy-web-engineer:20210719221049p:plain

反復して処理を行った結果が欲しい時に使えそうです。

Rails APIモードで kaminari を使ってみる

こんにちは!kossyです!




さて、今回はRails APIモードでの kaminari の使い方を備忘録としてブログに残してみたいと思います。




環境

Ruby 2.6.8
Rails 6.0.4
MacOS BIgSur
kaminari 1.2.1



実装

前提としてjbuilder gem が導入されているものとします。

まずはGemfileの編集

# Gemfile

gem 'kaminari'

で bundle install

$ bundle install

次にapp/controllers/concerns/ に pagination.rbという名前でモジュールを定義します。

$ touch app/controllers/concerns/pagination.rb

作成されたファイルを以下のように編集します。

module Pagination
  extend ActiveSupport::Concern

  def pagination(records)
    {
      total_count: records.total_count,
      limit_value: records.limit_value,
      total_pages: records.total_pages,
      current_page: records.current_page
    }
  end
end

Concernとしたのは、複数のcontrollersで使い回すことを想定しているためです。
ページネーションはどのcontrollersからでも要求される可能性のある機能ですので。

次に適当にscaffoldでCRUDを実装します。

$ rails g scaffold Post titie:string body:text

作成・編集されたファイルを適宜変更していきます。

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :set_post, only: :show

  include Pagination

  def index
    @posts = Post.page(params[:page]).per(params[:per])

    @pagination = pagination(@posts)
  end

  def show
  end

  private

    def set_post
      @post = Post.find(params[:id])
    end
end
# config/routes.rb

scope format: 'json' do
  resources :posts, only: [:index, :show]
end
# app/views/index.json.jbuilder

json.posts @posts, partial: 'posts/post', as: :post

json.pagination @pagination

次に適当にテストデータを作成します。

$ rails c

1.upto(100) do |i|
  Post.create!(title: "#{i}個目のタイトル", body: "#{i}個目の本文")
end

この状態でサーバーを立ち上げて、Postman等のAPIクライアントで動作確認をしてみます。

pageは2ページ目を、1ページ当たりに表示するレコードの数を5にしてリクエストを送信したため、IDが6 ~ 10のレコードを取得しています。

f:id:kossy-web-engineer:20210717174056p:plain


paginationも問題なく取得できています。(もともと5件テストデータ入ってて105件になってました、、、)

f:id:kossy-web-engineer:20210717174240p:plain

フロント側はパラメータとしてpageやperを送信し、サーバーから返ってきたtotal_count等のプロパティを使って、総件数や現在のページ番号を表示する実装になるかと思います。

招待機能を実現するGem「devise_invitable」のソースコードを追ってみる

こんにちは!kossyです!




今回はRailsで認証機能を実装する際の定番Gemである「devise」のextensionで、招待機能を実現するGem「devise_invitable」のソースコードを追ってみたので、ブログに残してみたいと思います。




github.com




環境
Ruby 2.6.6
Rails 6.0.3
MacOS catalina





なお、今回の記事ではdevise_invitableの導入手順については説明を割愛します。また、deviseを利用しているUserモデルが定義されていることとします。

導入手順については以下の記事が参考になるかと思います。

qiita.com


Devise::InvitationsController 各種prepend_before_action

github.com



createアクションやupdateアクションのコードを見たいところですが、4つほど定義されているprepend_before_actionから先に読みます。

prepend_before_actionはCallbackメソッドのひとつで、before_actionよりも前に呼び出されます。

authenticate_inviter!

github.com

    def authenticate_inviter!
      send(:"authenticate_#{resource_name}!", force: true)
    end

resource_nameはdeviseを利用しているモデル名の文字列で、authenticate_user!メソッドをsendメソッドで呼び出す処理でした。

要はログインしているかどうか?をprepend_before_actionで確認しているということですね。


has_invitations_left?

github.com

    def has_invitations_left?
      unless current_inviter.nil? || current_inviter.has_invitations_left?
        self.resource = resource_class.new
        set_flash_message :alert, :no_invitations_remaining if is_flashing_format?
        respond_with_navigational(resource) { render :new }
      end
    end

current_inviterは現在ログイン中のユーザーのことですね。

current_inviter.has_invitations_left?メソッドの処理は以下です。

    # Return true if this user has invitations left to send
    def has_invitations_left?
      if self.class.invitation_limit.present?
        if invitation_limit
          return invitation_limit > 0
        else
          return self.class.invitation_limit > 0
        end
      else
        return true
      end
    end

各ユーザーが招待可能な人数のリミットを指定するinvitation_limitカラムが存在し、0より大きいかどうかの比較に、

Devise.configで設定された値があればそちらを優先し、なければカラムの値を使って比較して真偽値を返却します。

controller側のhas_invitations_left?に戻ります。

resource_classはdeviseを使っているモデルそのもので、今回の場合はUserモデルのインスタンスですね。

その後は内部でrespond_withメソッドを呼び出す respond_with_navigationalメソッドを用いてnewのviewをrenderしています。


require_no_authentication

github.com

  def require_no_authentication
    assert_is_devise_resource!
    return unless is_navigational_format?
    no_input = devise_mapping.no_input_strategies

    authenticated = if no_input.present?
      args = no_input.dup.push scope: resource_name
      warden.authenticate?(*args)
    else
      warden.authenticated?(resource_name)
    end

    if authenticated && resource = warden.user(resource_name)
      set_flash_message(:alert, 'already_authenticated', scope: 'devise.failure')
      redirect_to after_sign_in_path_for(resource)
    end
  end

assert_is_devise_resource!メソッドは、操作を行っているresourceがdeviseがmappingされたものであるかどうかを確認していました。

is_navigational_format?メソッドは、request_formatがDevise.navigational_formatsに含まれているかどうかを真偽値で返すようにしていました。

残りの処理は、既に認証済みの場合はフラッシュメッセージに「既に認証済みである」旨のメッセージを格納し、リダイレクト処理を行っていました。


resource_from_invitation_token

    def resource_from_invitation_token
      unless params[:invitation_token] && self.resource = resource_class.find_by_invitation_token(params[:invitation_token], true)
        set_flash_message(:alert, :invitation_token_invalid) if is_flashing_format?
        redirect_to after_sign_out_path_for(resource_name)
      end
    end

招待トークンがnilまたはdeviseを使っているモデルのテーブルを招待トークンで検索してレコードが見つからない場合は、
フラッシュメッセージに「招待トークンが間違っている」旨のメッセージを格納し、リダイレクト処理を行っていました。

before_actionを読むだけでもかなりのボリュームになりますね、、、


createアクションのソースコードを読む

ようやくcreateアクションのコードにたどり着きました、、、

  # POST /resource/invitation
  def create
    self.resource = invite_resource
    resource_invited = resource.errors.empty?

    yield resource if block_given?

    if resource_invited
      if is_flashing_format? && self.resource.invitation_sent_at
        set_flash_message :notice, :send_instructions, email: self.resource.email
      end
      if self.method(:after_invite_path_for).arity == 1
        respond_with resource, location: after_invite_path_for(current_inviter)
      else
        respond_with resource, location: after_invite_path_for(current_inviter, resource)
      end
    else
      respond_with_navigational(resource) { render :new }
    end
  end

invite_resourceメソッドを読んでみます。

    def invite_resource(&block)
      resource_class.invite!(invite_params, current_inviter, &block)
    end

    # invite_paramsのソースコードは下記

    def invite_params
      devise_parameter_sanitizer.sanitize(:invite)
    end

    # current_inviterメソッドは処理の内部でauthenticate_inviter!メソッドをコールしていました
    # https://github.com/scambra/devise_invitable/blob/db1f065c452e6102ff8802bb264329adc4714295/lib/devise_invitable/controllers/helpers.rb#L17

    def authenticate_inviter!
      send(:"authenticate_#{resource_name}!", force: true)
    end

invite!メソッドを見にいきましょう。

        def invite!(attributes = {}, invited_by = nil, options = {}, &block)
          attr_hash = ActiveSupport::HashWithIndifferentAccess.new(attributes.to_h)
          _invite(attr_hash, invited_by, options, &block).first
        end

_inviteメソッドが本丸っぽい。

        # Attempt to find a user by its email. If a record is not found,
        # create a new user and send an invitation to it. If the user is found,
        # return the user with an email already exists error.
        # If the user is found and still has a pending invitation, invitation
        # email is resent unless resend_invitation is set to false.
        # Attributes must contain the user's email, other attributes will be
        # set in the record
        def _invite(attributes = {}, invited_by = nil, options = {}, &block)
          invite_key_array = invite_key_fields
          attributes_hash = {}
          invite_key_array.each do |k,v|
            attribute = attributes.delete(k)
            attribute = attribute.to_s.strip if strip_whitespace_keys.include?(k)
            attributes_hash[k] = attribute
          end

          invitable = find_or_initialize_with_errors(invite_key_array, attributes_hash)
          invitable.assign_attributes(attributes)
          invitable.invited_by = invited_by
          unless invitable.password || invitable.encrypted_password.present?
            invitable.password = random_password
          end

          invitable.valid? if self.validate_on_invite
          if invitable.new_record?
            invitable.clear_errors_on_valid_keys if !self.validate_on_invite
          elsif invitable.invitation_taken? || !self.resend_invitation
            invite_key_array.each do |key|
              invitable.add_taken_error(key)
            end
          end

          yield invitable if block_given?
          mail = invitable.invite!(nil, options) if invitable.errors.empty?
          [invitable, mail]
        end

コメントアウト部分を訳してみます。

電子メールでユーザーを見つけようとします。 レコードが見つからない場合は、新しいユーザーを作成して招待状を送信します。 ユーザーが見つかった場合は、メールが既に存在するというエラーでユーザーを返します。

ユーザーが見つかり、まだ保留中の招待がある場合、resend_invitationがfalseに設定されていない限り、招待メールが再送信されます。 属性にはユーザーの電子メールが含まれている必要があり、他の属性はレコードに設定されます。

非常に丁寧なコメントアウトですね、、、自分もこのようなコメントアウトが英語で書けるようになりたいものです。もはやコードの説明は不要でしょう。(面倒だから読みたくないだけ)

ようやくinvite_resourceメソッドを読み終えました。残りの処理はエラーの有無に応じて、遷移先のページを切り替えているようでした。


大いに参考にさせていただいた記事

この場を借りて御礼を申し上げます。

Railsで、deviseとdevise_invitableをつかって招待機能を実装する - Qiita

ファイルアップロードを行うGem 「Shrine」の紹介

こんにちは!kossyです!




さて、今回はファイルアップロードを行うGem 「Shrine」の使い方をブログに残してみたいと思います。



環境

Ruby 2.6.6
Rails 6.0.4
shrine 3.4.0



実装

公式のGettingStartedに倣って実装していきます。

Getting Started · Shrine



まずはGemfileの編集

# Gemfile

gem 'shrine'

でbundle install

$ bundle install

config/initializers/shrine.rbを作成します。

$ touch config/initializers/shrine.rb

作成したファイルを以下のように編集します。

# config/initializers/shrine.rb

require "shrine"
require "shrine/storage/file_system"
 
Shrine.storages = { 
  cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary 
  store: Shrine::Storage::FileSystem.new("public", prefix: "uploads"),       # permanent 
}
 
Shrine.plugin :sequel # or :activerecord 
Shrine.plugin :cached_attachment_data # for retaining the cached file across form redisplays 
Shrine.plugin :restore_cached_data # re-extract metadata when attaching a cached file 
Shrine.plugin :rack_file # for non-Rails apps 

次にモデル(今回はApplicationRecordを継承したEmployeeモデルが存在するとします)の添付ファイル属性に名前を付ける方法を決定し、_dataテキストまたはJSON列を追加するマイグレーションファイルを作成します。

$ rails g migration AddImageDataToEmployee image_data:text
class AddImageDataToEmployee < ActiveRecord::Migration[6.0]
  def change
    add_column :employees, :image_data, :text
  end
end
$ rails db:migrate

次に、ファイルアップローダークラスを作成します。このクラスはshrineを継承して作成します。

$ mkdir app/uploader

$ touch app/uploader/image_uploader.rb
# app/uploader/image_uploader.rb

class ImageUploader < Shrine
end

次に、Employeeモデルにファイルアップロードに関する仮想的な属性を定義します。

# app/models/employee.rb

class Employee < ApplicationRecord
  include ImageUploader::Attachment(:image)
end

これでshrineを介したimageの読み書きが可能になります。

画像のリサイズ

画像の加工はimagemagickを用いて実装していきます。

$ brew install imagemagick
# Gemfile

gem 'image_processing'
gem 'mini_magick'
$ bundle install

config/initializers/shrine.rbにオプションを追加します。

# config/initializers/shrine.rb

require "shrine"
require "shrine/storage/file_system"

Shrine.storages = {
  cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
  store: Shrine::Storage::FileSystem.new("public", prefix: "uploads"),       # permanent
}

Shrine.plugin :activerecord
Shrine.plugin :cached_attachment_data # for retaining the cached file across form redisplays
Shrine.plugin :restore_cached_data # re-extract metadata when attaching a cached file
Shrine.plugin :rack_file # for non-Rails apps
Shrine.plugin :derivatives, create_on_promote: true # 追加

次に、先ほど作成したImageUploaderクラスを修正します。

require "image_processing/mini_magick"
 
class ImageUploader < Shrine
  Attacher.derivatives do |original|
    magick = ImageProcessing::MiniMagick.source(original)
 
    { 
      large:  magick.resize_to_limit!(800, 800),
      medium: magick.resize_to_limit!(500, 500),
      small:  magick.resize_to_limit!(300, 300),
    }
  end
end

まとめ

公式のGettingStartedがかなり丁寧なので、最低限の導入であればそれほどてこずることなくできそうです。

TailwindCSS でタグ表示っぽい要素をつくる

こんにちは!kossyです!




今回はTailwindCSS でタグ表示っぽい要素をつくったので、ブログに残してみたいと思います!



環境

Vue.js 3系
tailwindcss/postcss7-compat@^2.0.3




完成品としてはこんなものを目指します。

f:id:kossy-web-engineer:20210710085329p:plain


実装

まずはコード全晒しです。

  <aside class="w-full text-xl text-gray-800 leading-normal">
    <div class="pb-8 mt-8">
      <p class="tracking-wide leading-7 text-gray-500">注目のタグ</p>
    </div>
    <div class="flex flex-wrap items-start">
      <div class="flex mb-6">
        <a class="mr-4">
          <div class="px-2 py-4 rounded-2xl relative bg-gray-100">
            <p class="text-gray-900 leading-6 text-xs">Technology</p>
          </div>
        </a>
      </div>
      <div class="flex mb-6">
        <a class="mr-4">
          <div class="px-2 py-4 rounded-2xl relative bg-gray-100">
            <p class="text-gray-900 leading-6 text-xs">General</p>
          </div>
        </a>
      </div>
      <div class="flex mb-6">
        <a class="mr-4">
          <div class="px-2 py-4 rounded-2xl relative bg-gray-100">
            <p class="text-gray-900 leading-6 text-xs">Technology</p>
          </div>
        </a>
      </div>
      <div class="flex mb-6">
        <a class="mr-4">
          <div class="px-2 py-4 rounded-2xl relative bg-gray-100">
            <p class="text-gray-900 leading-6 text-xs">Technology</p>
          </div>
        </a>
      </div>
      <div class="flex mb-6">
        <a class="mr-4">
          <div class="px-2 py-4 rounded-2xl relative bg-gray-100">
            <p class="text-gray-900 leading-6 text-xs">Technology</p>
          </div>
        </a>
      </div>
      <div class="flex mb-6">
        <a class="mr-4">
          <div class="px-2 py-4 rounded-2xl relative bg-gray-100">
            <p class="text-gray-900 leading-6 text-xs">Technology</p>
          </div>
        </a>
      </div>
    </div>
  </aside>

もしJavaScriptフレームワークを使っていれば、for文で繰り返しで表示するのが筋かと思いますが、一旦仮置きしています。

使ったTailwindCSSのクラスを列挙します。

クラス プロパティ
w-full width: 100%;
text-xs font-size: 0.75rem;
line-height: 1rem;
text-xl font-size: 1.25rem;
line-height: 1.75rem;
text-gray-500 --tw-text-opacity: 1;
color: rgba(107, 114, 128, var(--tw-text-opacity));
text-gray-800 --tw-text-opacity: 1;
color: rgba(31, 41, 55, var(--tw-text-opacity));
text-gray-900 --tw-text-opacity: 1;
color: rgba(17, 24, 39, var(--tw-text-opacity));
bg-gray-100 --tw-bg-opacity: 1;
background-color: rgba(243, 244, 246, var(--tw-bg-opacity));
leading-6 line-height: 1.5rem;
leading-7 line-height: 1.75rem;
leading-normal line-height: 1.5;
px-2 padding-left: 0.5rem;
padding-right: 0.5rem;
py-4 padding-top: 1rem;
padding-bottom: 1rem;
pb-8 padding-bottom: 2rem;
mr-4 margin-right: 1rem;
mb-6 margin-bottom: 1.5rem;
mt-8 margin-top: 2rem;
tracking-wide letter-spacing: 0.025em;
flex display: flex;
flex-wrap flex-wrap: wrap;
items-start align-items: flex-start;
rounded-2xl border-radius: 1rem;
relative position: relative;

まとめ

基本的に、いろんなサイトを見て「これ良い!!」って思ったら、chromeの検証ツールでCSSを見て、使われているCSSをTailwindCSSのclassに置き換える、をするのが一番近道かと思います。

HTMLでclassに書き込むだけで簡単にスタイリングできるのはほんと楽ちんです。


参考にさせていただいたサイト

Medium – Where good ideas find you.

Vue.js 3 + TypeScriptでvue-qrcodeを導入する

こんにちは!kossyです!




さて、今回はVue.js 3 + TypeScriptでvue-qrcodeを導入する方法について、ブログに残してみたいと思います。



環境

vue/cli 4.5.9
vue 3.1.2
typescript 3.9.7
vue-qrcode 2.0.0-rc




導入

以下のコマンドを実行します。

$ npm install vue@next qrcode @chenfengyuan/vue-qrcode@next

npm install が完了したら、main.tsを編集します。

import { createApp } from 'vue'
import App from '@/App.vue'
import VueQrcode from '@chenfengyuan/vue-qrcode'

const app = createApp(App)

if (VueQrcode.name) {
  app.component(VueQrcode.name, VueQrcode)
}

app.mount('#app')

if文で条件分岐しているのは、VueQrcode.nameの型が string | undefined になっていて、app.component関数の第一引数の型と合わないとTypeScript Compilerに指摘を受けるためです。

f:id:kossy-web-engineer:20210703215421p:plain

Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
  Type 'undefined' is not assignable to type 'string'.

と言われてしまっているため、if文で分岐をいれて指摘をされないようにしています。

あとは、以下のようにtemplateタグで定義すればQRコードを表示することができます。

<vue-qrcode :value="qrCode" />

"qrCode"にはuriを格納している想定です。


大いに参考にさせていただいたサイト

GitHub - fengyuanchen/vue-qrcode: QR code component for Vue.js

権限管理Gem「Pundit」の権限判定処理を直接呼び出したい

こんにちは!kossyです!




今回は権限管理Gem「Pundit」の権限判定処理を直接呼び出したい時の書き方について、ブログに残してみたいと思います。




環境

Ruby 2.6.6
Rails 6.0.3
MacOS Catalina




実装

通常、PunditはController内でauthorizeメソッドと共に呼び出すことが多いと思いますが、Controller以外の場所で直接呼び出したい場合はどうすればいいでしょうか。

結論、以下のように書けばOKです。

# 既にUserPolicyが作成されていて、show?メソッドが定義されているのを前提としています。

Pundit.policy!(current_user, User).show?

policy!メソッドの第一引数に現在ログイン中のユーザーを、第二引数にPolicyを実行したいclass名を渡し、そのPolicyクラス(上記の例だとUserPolicy)に定義された権限判定のメソッドを呼び出せばOKです。

policy!メソッドの中身を覗いてみる

ついでにGem内の実装を確認してみましょう。

github.com

    def policy!
      policy or raise NotDefinedError, "unable to find policy `#{find(object)}` for `#{object.inspect}`"
    end

policyメソッドも見る必要がありそうなので、確認してみます。

    def policy
      klass = find(object)
      klass.is_a?(String) ? klass.safe_constantize : klass
    end

実行するPolicyクラスを見つけに行く処理ですね。

Policy!メソッドの方は、引数に渡したPolicyが見つからない場合は、例外を返すようになっています。

policyとpolicy!の使い分けは、引数に渡したPolicyが見つからない場合に例外が欲しいかどうかで分ければよさそうです。




勉強になりました。