Skip to content

Commit

Permalink
Use model scopes with Ransack (coded by @avit and @glebm).
Browse files Browse the repository at this point in the history
  • Loading branch information
Sven Schwyn committed Jun 22, 2014
1 parent 17abebc commit 72dd5d1
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 3 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/ransack/adapters/active_record/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ def ransackable_associations(auth_object = nil)
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
Expand Down
17 changes: 17 additions & 0 deletions lib/ransack/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ def contextualize(str)
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
Expand Down Expand Up @@ -143,6 +156,10 @@ def ransackable_association?(str, klass)
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
Expand Down
29 changes: 26 additions & 3 deletions lib/ransack/search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def initialize(object, params = {}, options = {})
@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

Expand All @@ -33,6 +34,8 @@ def build(params)
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
Expand Down Expand Up @@ -82,20 +85,40 @@ def new_sort(opts = {})

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?("(")
Expand Down
39 changes: 39 additions & 0 deletions spec/ransack/adapters/active_record/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,39 @@ module ActiveRecord
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
Expand Down Expand Up @@ -233,6 +266,12 @@ def self.sane_adapter?
it { should include 'articles' }
end

describe '#ransackable_scopes' do
subject { Person.ransackable_scopes }

it { should eq [] }
end

end
end
end
Expand Down
4 changes: 4 additions & 0 deletions spec/support/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 72dd5d1

Please sign in to comment.