activerecord-hackery / ransack

Object-based searching.
https://activerecord-hackery.github.io/ransack/
MIT License
5.65k stars 795 forks source link

【Question】 If pass arguments "1" value to the scope method for ransackable_scopes, ArgumentError (wrong number of arguments (given 0, expected 1)): #1232

Open tayagi-aim opened 3 years ago

tayagi-aim commented 3 years ago

Version

Source code

Model

app/model/user.rb

class User < ApplicationRecord

  has_one :invitation, dependent: :destroy
  has_many :invitees, through: :invitation
  has_many :invitees_invitation_acceptances, through: :invitation, source: :invitation_acceptances
  has_many :invitees_invitation_players, -> { where(invitation_achievement_id: INVITATION_PLAYER) }, through: :invitees_invitation_acceptances, source: :invitation_awards

  has_one :invitation_acceptance, foreign_key: :link_auth_id, primary_key: :link_auth_id, dependent: :destroy
  has_one :inviter, through: :invitation_acceptance
  has_many :invitation_awards, through: :invitation_acceptance

  scope :invitation_player_gteq, ->(value) { having("COUNT(invitation_awards.id) >= ?", value) }
  scope :invitation_player_lt, ->(value) { having("COUNT(invitation_awards.id) < ?", value) }

  private

  class << self
    def ransackable_scopes(auth_object = nil)
      [:invitation_player_gteq, :invitation_player_lt, :invitation_complete_gteq, :invitation_complete_lt]
    end
  end
end

Controller

  class UsersController < ApplicationController

    def invites
      user = if invite_search_params[:s]&.include?("invitation_complete_count") || !invite_search_params["invitation_complete_gteq"]&.empty? || !invite_search_params["invitation_complete_lt"]&.empty?
               User.left_joins(:invitees_invitation_completes).group(:id)
             else
               User.left_joins(:invitees_invitation_players).group(:id)
             end
      @q = user.ransack(invite_search_params)
      @users = @q.result.page(params[:page])
    end

    private

    def invite_search_params
      params.fetch(:q, {}).permit(
        :uid_eq,
        :identifier_cont,
        :invitation_player_gteq,
        :invitation_player_lt,
        :invitation_complete_gteq,
        :invitation_complete_lt,
        :s,
      )
    end
end

If not "1" arguments

debug

[2] pry(#<*****>)> params
=> <***** {"utf8"=>"✓", "q"=>{ "invitation_player_gteq"=>"2"}, "commit"=>"検索", "controller"=>"admin/users", "action"=>"invites"} permitted: false>
80:   scope :invitation_player_gteq, ->(value) { binding.pry
 => 81:   having("COUNT(invitation_awards.id) >= ?", value) }

[1] pry(#<*****>)> value
=> "2"

But If "1" arguments

[2] pry(#<*****>)> params
=> <ActionController::Parameters {"utf8"=>"✓", "q"=>{ "invitation_player_gteq"=>"1"}, "commit"=>"検索", "controller"=>"admin/users", "action"=>"invites"} permitted: false>
  scope :invitation_player_gteq, ->(value) { binding.pry
  having("COUNT(invitation_awards.id) >= ?", value) }

ArgumentError (wrong number of arguments (given 0, expected 1)):

Why If pass arguments "1" value to the scope method for ransackable_scopes, Argument Error?

tom-lord commented 3 years ago

What's the full error trace? Only showing the top line of the error with no context makes this difficult to understand.

tayagi-aim commented 3 years ago

@tom-lord

Hi, Thanks for reply.

Full Trace

I did attach full trace log for rails.

tayagi-aim commented 3 years ago

@tom-lord

Hi. I had reading source code for ransack.

Therefore, I did notice that "1" character did convert from "1" to "true" for sanitize scope method.

https://github.com/activerecord-hackery/ransack/blob/16ce9110cbaee6682aca6e06ccb9d66a0c7a46c3/lib/ransack/search.rb#L128

    def add_scope(key, args)
      sanitized_args = if Ransack.options[:sanitize_scope_args] && !@context.ransackable_scope_skip_sanitize_args?(key, @context.object)
        sanitized_scope_args(args)
      else
        args
      end

      if @context.scope_arity(key) == 1
        @scope_args[key] = args.is_a?(Array) ? args[0] : args
      else
        @scope_args[key] = args.is_a?(Array) ? sanitized_args : args
      end
      @context.chain_scope(key, sanitized_args)
    end

    def sanitized_scope_args(args)
      if args.is_a?(Array)
        args = args.map(&method(:sanitized_scope_args))
      end

      if Constants::TRUE_VALUES.include? args
        true
      elsif Constants::FALSE_VALUES.include? args
        false
      else
        args
      end
    end

https://github.com/activerecord-hackery/ransack/blob/16ce9110cbaee6682aca6e06ccb9d66a0c7a46c3/lib/ransack/context.rb#L47

    def chain_scope(scope, args)
      return unless @klass.method(scope) && args != false
      @object = if scope_arity(scope) < 1 && args == true
                  @object.public_send(scope)
                else
                  @object.public_send(scope, *args)
                end
    end

Debug Result

    130: def add_scope(key, args)
    131:   binding.pry
 => 132:   sanitized_args = if Ransack.options[:sanitize_scope_args] && !@context.ransackable_scope_skip_sanitize_args?(key, @context.object)
    133:     sanitized_scope_args(args)
    134:   else
    135:     args
    136:   end
    137:
    138:   if @context.scope_arity(key) == 1
    139:     @scope_args[key] = args.is_a?(Array) ? args[0] : args
    140:   else
    141:     @scope_args[key] = args.is_a?(Array) ? sanitized_args : args
    142:   end
    143:   @context.chain_scope(key, sanitized_args)
    144: end

[1] pry(#<Ransack::Search>)> args
=> "1"

    47: def chain_scope(scope, args)
    48:   binding.pry
 => 49:   return unless @klass.method(scope) && args != false
    50:   @object = if scope_arity(scope) < 1 && args == true
    51:               @object.public_send(scope)
    52:             else
    53:               @object.public_send(scope, *args)
    54:             end
    55: end

[1] pry(#<Ransack::Adapters::ActiveRecord::Context>)> args
=> true

    47: def chain_scope(scope, args)
    48:   binding.pry
    49:   return unless @klass.method(scope) && args != false
    50:   @object = if scope_arity(scope) < 1 && args == true
 => 51:               @object.public_send(scope)
    52:             else
    53:               @object.public_send(scope, *args)
    54:             end
    55: end

ArgumentError (wrong number of arguments (given 0, expected 1)):

By this sanitize scope method, For what reason "1" character convert from "1" to "true"?

synion commented 3 years ago

Is there any other solution, better than adding a default value to the scope eg. ? scope :scope_name, ->(string = '1') { sql } Even more, if you pass '0' to the ransack scope this scope isn't called at all.

synion commented 3 years ago

Solved in https://github.com/activerecord-hackery/ransack/issues/924, just add

def self.ransackable_scopes_skip_sanitize_args
  [:scope_name]
end

to the model.

f-g-p commented 2 years ago

Hi, I noticed I have the same issue with the letter t, and not just with 1

and when I replace ransackable_scopes with ransackable_scopes_skip_sanitize_args, then my ransack start behaving wrongly.

Adding my repro app to help:

versions

ransack: 3.0.0 rails: ~> 7.0.0 ruby: 3.0.0p0

model

class User < ApplicationRecord
  scope :by_first_or_last_name, lambda { |name|
                                  where('lower(first_name) LIKE :prefix', prefix: "#{name.downcase}%")
                                    .or(where('lower(last_name) LIKE :prefix', prefix: "#{name.downcase}%"))
                                }

  def self.ransackable_scopes(_auth_object = nil)
    [:by_first_or_last_name]
  end
end

controller

class UsersController < ApplicationController
  def index
    users = User.ransack(by_first_or_last_name: params[:by_first_or_last_name]).result
    render json: users
  end
end

test

class UsersControllerTest < ActionDispatch::IntegrationTest
  test 'should get index' do
    User.create!(first_name: 'John', last_name: 'Doe')
    User.create!(first_name: 'Joe', last_name: 'Dohn')
    get '/users'
    assert_response :success
    response_body = JSON.parse(response.body)
    assert_equal(response_body.size, 2)
  end

  test 'should get index with filter' do
    User.create!(first_name: 'John', last_name: 'Doe')
    User.create!(first_name: 'Joe', last_name: 'Dohn')
    get '/users?by_first_or_last_name=John'
    assert_response :success
    response_body = JSON.parse(response.body)
    # new issue here:
    assert_equal(response_body.size, 1) # fails with ransackable_scopes_skip_sanitize_args (returns both users instead of the only matching one)
  end

  test 'should get index with filter - fails with letter t o_O' do # my initial issue
    User.create!(first_name: 'John', last_name: 'Doe')
    User.create!(first_name: 'Joe', last_name: 'Dohn')
    assert_raise do
      get '/users?by_first_or_last_name=t'
    end
  end
end