Use model scopes with Ransack (coded by @avit and @glebm).

This commit is contained in:
Sven Schwyn 2014-06-22 11:15:10 +02:00
parent 17abebce82
commit 72dd5d12d5
6 changed files with 122 additions and 3 deletions

View File

@ -278,6 +278,37 @@ ENV['RANSACK_FORM_BUILDER'] = '::SimpleForm::FormBuilder'
require 'rails/all'
```
### Authorization
By default Ransack exposes search for any model column, so take care to
sanitize params and only pass allowed keys. Alternately, you can define these
methods on your model classes for applying selective authorization based on a
given auth object:
* `def ransackable_attributes(auth_object = nil)`
* `def ransackable_associations(auth_object = nil)`
* `def ransackable_scopes(auth_object = nil)`
* `def ransortable_attributes(auth_object = nil)` (for sorting)
Any values not included in the arrays returned from these methods will be
ignored. The auth object should be optional when building the search, and is
ignored by default:
```
Employee.search({'salary_gt' => 100000}, {auth_object: current_user})
```
### Scopes
Searching by scope requires defining a whitelist of `ransackable_scopes` on the
model class. 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:
```
Employee.search({'active' => true, 'hired_since' => '2013-01-01'})
```
### I18n
Ransack translation files are available in

View File

@ -36,6 +36,11 @@ module Ransack
reflect_on_all_associations.map { |a| a.name.to_s }
end
# For overriding with a whitelist of symbols
def ransackable_scopes(auth_object = nil)
[]
end
end
end
end

View File

@ -77,6 +77,19 @@ module Ransack
table_for(parent)[attr_name]
end
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
def scope_arity(scope)
@klass.method(scope).arity
end
def bind(object, str)
object.parent, object.attr_name = @bind_pairs[str]
end
@ -143,6 +156,10 @@ module Ransack
klass.ransackable_associations(auth_object).include? str
end
def ransackable_scope?(str, klass)
klass.ransackable_scopes(auth_object).any? { |s| s.to_s == str }
end
def searchable_attributes(str = '')
traverse(str).ransackable_attributes(auth_object)
end

View File

@ -20,6 +20,7 @@ module Ransack
@context = Context.for(object, options)
@context.auth_object = options[:auth_object]
@base = Nodes::Grouping.new(@context, 'and')
@scope_args = {}
build(params.with_indifferent_access)
end
@ -33,6 +34,8 @@ module Ransack
send("#{key}=", value)
elsif base.attribute_method?(key)
base.send("#{key}=", value)
elsif @context.ransackable_scope?(key, @context.object)
add_scope(key, value)
elsif !Ransack.options[:ignore_unknown_conditions]
raise ArgumentError, "Invalid search term #{key}"
end
@ -82,20 +85,40 @@ module Ransack
def method_missing(method_id, *args)
method_name = method_id.to_s
writer = method_name.sub!(/\=$/, '')
if base.attribute_method?(method_name)
getter_name = method_name.sub(/=$/, '')
if base.attribute_method?(getter_name)
base.send(method_id, *args)
elsif @context.ransackable_scope?(getter_name, @context.object)
if method_name =~ /=$/
add_scope getter_name, args
else
@scope_args[method_name]
end
else
super
end
end
def inspect
"Ransack::Search<class: #{klass.name}, base: #{base.inspect}>"
details = [
[:class, klass.name],
([:scope, @scope_args] if @scope_args.present?),
[:base, base.inspect]
].compact.map { |d| d.join(': ') }.join(', ')
"Ransack::Search<#{details}>"
end
private
def add_scope(key, args)
if @context.scope_arity(key) == 1
@scope_args[key] = args.is_a?(Array) ? args[0] : args
else
@scope_args[key] = args
end
@context.chain_scope(key, args)
end
def collapse_multiparameter_attributes!(attrs)
attrs.keys.each do |k|
if k.include?("(")

View File

@ -17,6 +17,39 @@ module Ransack
it 'has a Relation as its object' do
expect(subject.object).to be_an ::ActiveRecord::Relation
end
context 'with scopes' do
before do
Person.stub :ransackable_scopes => [:active, :over_age]
end
it "applies true scopes" do
search = Person.search('active' => true)
search.result.to_sql.should include "active = 1"
end
it "ignores unlisted scopes" do
search = Person.search('restricted' => true)
search.result.to_sql.should_not include "restricted"
end
it "ignores false scopes" do
search = Person.search('active' => false)
search.result.to_sql.should_not include "active"
end
it "passes values to scopes" do
search = Person.search('over_age' => 18)
search.result.to_sql.should include "age > 18"
end
it "chains scopes" do
search = Person.search('over_age' => 18, 'active' => true)
search.result.to_sql.should include "age > 18"
search.result.to_sql.should include "active = 1"
end
end
end
describe '#ransacker' do
@ -233,6 +266,12 @@ module Ransack
it { should include 'articles' }
end
describe '#ransackable_scopes' do
subject { Person.ransackable_scopes }
it { should eq [] }
end
end
end
end

View File

@ -36,6 +36,10 @@ class Person < ActiveRecord::Base
:source => :comments, :foreign_key => :person_id
has_many :notes, :as => :notable
scope :restricted, lambda { where("restricted = 1") }
scope :active, lambda { where("active = 1") }
scope :over_age, lambda { |y| where(["age > ?", y]) }
ransacker :reversed_name, :formatter => proc { |v| v.reverse } do |parent|
parent.table[:name]
end