-->

Ransack: How to use existing scope?

2019-03-18 13:33发布

问题:

Converting a Rails 2 application to Rails 3, I have to replace the gem searchlogic. Now, using Rails 3.2.8 with the gem Ransack I want to build a search form which uses an existing scope. Example:

class Post < ActiveRecord::Base
  scope :year, lambda { |year| 
    where("posts.date BETWEEN '#{year}-01-01' AND '#{year}-12-31'") 
  }
end  

So far as I know, this can be achieved by defining a custom ransacker. Sadly, I don't find any documentation about this. I tried this in the Postclass:

ransacker :year, 
          :formatter => proc {|v| 
            year(v)
          }

But this does not work:

Post.ransack(:year_eq => 2012).result.to_sql
=> TypeError: Cannot visit ActiveRecord::Relation

I tried some variations of the ransacker declaration, but none of them work. I Need some help...

UPDATE: The scope above is just on example. I'm looking for a way to use every single existing scope within Ransack. In MetaSearch, the predecessor of Ransack, there is a feature called search_methods for using scopes. Ransack has no support for this out of the box yet.

回答1:

ransack supports it out of the box after merging https://github.com/activerecord-hackery/ransack/pull/390 . you should declare ransakable_scopes method to add scopes visible for ransack.

From manual

Continuing on from the preceding section, searching by scopes requires defining a whitelist of ransackable_scopes on the model class. The whitelist should be an array of symbols. By default, all class methods (e.g. scopes) are ignored. Scopes will be applied for matching true values, or for given values if the scope accepts a value:

class Employee < ActiveRecord::Base
  scope :activated, ->(boolean = true) { where(active: boolean) }
  scope :salary_gt, ->(amount) { where('salary > ?', amount) }

  # Scopes are just syntactical sugar for class methods, which may also be used:

  def self.hired_since(date)
    where('start_date >= ?', date)
  end

  private

  def self.ransackable_scopes(auth_object = nil)
    if auth_object.try(:admin?)
      # allow admin users access to all three methods
      %i(activated hired_since salary_gt)
    else
      # allow other users to search on `activated` and `hired_since` only
      %i(activated hired_since)
    end
  end
end

Employee.ransack({ activated: true, hired_since: '2013-01-01' })

Employee.ransack({ salary_gt: 100_000 }, { auth_object: current_user })


回答2:

Ransack let's you create custom predicates for this, unfortunately the documentation leaves room for improvement however checkout: https://github.com/ernie/ransack/wiki/Custom-Predicates

Also I believe the problem you're trying to tackle is up on their issue tracker. There's a good discussion going on there: https://github.com/ernie/ransack/issues/34



回答3:

I wrote a gem called siphon which helps you translate parameters into activerelation scopes. Combining it with ransack can achieves this.

You can read full explanation here. Meanwhile here's the gist of it

The View

= form_for @product_search, url: "/admin/products", method: 'GET' do |f|
  = f.label "has_orders"
  = f.select :has_orders, [true, false], include_blank: true
  -#
  -# And the ransack part is right here... 
  -#
  = f.fields_for @product_search.q, as: :q do |ransack|
    = ransack.select :category_id_eq, Category.grouped_options
```

ok so now params[:product_search] holds the scopes and params[:product_search][:q] has the ransack goodness. We need to find a way, now, to distribute that data to the form object. So first let ProductSearch swallow it up in the controller:

The Controller

# products_controller.rb
def index
  @product_search = ProductSearch.new(params[:product_search])
  @products ||= @product_formobject.result.page(params[:page])
end

The Form Object

# product_search.rb
class ProductSearch
  include Virtus.model
  include ActiveModel::Model

  # These are Product.scopes for the siphon part
  attribute :has_orders,    Boolean
  attribute :sort_by,       String

  # The q attribute is holding the ransack object
  attr_accessor :q

  def initialize(params = {})
    @params = params || {}
    super
    @q = Product.search( @params.fetch("q") { Hash.new } )
  end

  # siphon takes self since its the formobject
  def siphoned
    Siphon::Base.new(Product.scoped).scope( self )
  end

  # and here we merge everything
  def result
    Product.scoped.merge(q.result).merge(siphoned)
  end
end