ransack 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +11 -0
- data/LICENSE +20 -0
- data/README.rdoc +5 -0
- data/Rakefile +19 -0
- data/lib/ransack.rb +24 -0
- data/lib/ransack/adapters/active_record.rb +2 -0
- data/lib/ransack/adapters/active_record/base.rb +17 -0
- data/lib/ransack/adapters/active_record/context.rb +153 -0
- data/lib/ransack/configuration.rb +39 -0
- data/lib/ransack/constants.rb +23 -0
- data/lib/ransack/context.rb +152 -0
- data/lib/ransack/helpers.rb +2 -0
- data/lib/ransack/helpers/form_builder.rb +172 -0
- data/lib/ransack/helpers/form_helper.rb +27 -0
- data/lib/ransack/locale/en.yml +67 -0
- data/lib/ransack/naming.rb +53 -0
- data/lib/ransack/nodes.rb +7 -0
- data/lib/ransack/nodes/and.rb +8 -0
- data/lib/ransack/nodes/attribute.rb +36 -0
- data/lib/ransack/nodes/condition.rb +209 -0
- data/lib/ransack/nodes/grouping.rb +207 -0
- data/lib/ransack/nodes/node.rb +34 -0
- data/lib/ransack/nodes/or.rb +8 -0
- data/lib/ransack/nodes/sort.rb +39 -0
- data/lib/ransack/nodes/value.rb +120 -0
- data/lib/ransack/predicate.rb +57 -0
- data/lib/ransack/search.rb +114 -0
- data/lib/ransack/translate.rb +92 -0
- data/lib/ransack/version.rb +3 -0
- data/ransack.gemspec +29 -0
- data/spec/blueprints/articles.rb +5 -0
- data/spec/blueprints/comments.rb +5 -0
- data/spec/blueprints/notes.rb +3 -0
- data/spec/blueprints/people.rb +4 -0
- data/spec/blueprints/tags.rb +3 -0
- data/spec/console.rb +22 -0
- data/spec/helpers/ransack_helper.rb +2 -0
- data/spec/playground.rb +37 -0
- data/spec/ransack/adapters/active_record/base_spec.rb +30 -0
- data/spec/ransack/adapters/active_record/context_spec.rb +29 -0
- data/spec/ransack/configuration_spec.rb +11 -0
- data/spec/ransack/helpers/form_builder_spec.rb +39 -0
- data/spec/ransack/nodes/compound_condition_spec.rb +0 -0
- data/spec/ransack/nodes/condition_spec.rb +0 -0
- data/spec/ransack/nodes/grouping_spec.rb +13 -0
- data/spec/ransack/predicate_spec.rb +25 -0
- data/spec/ransack/search_spec.rb +182 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/schema.rb +102 -0
- metadata +200 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'ransack/nodes'
|
2
|
+
require 'ransack/context'
|
3
|
+
require 'ransack/naming'
|
4
|
+
|
5
|
+
module Ransack
|
6
|
+
class Search
|
7
|
+
include Naming
|
8
|
+
|
9
|
+
attr_reader :base, :context
|
10
|
+
|
11
|
+
delegate :object, :klass, :to => :context
|
12
|
+
delegate :new_and, :new_or, :new_condition,
|
13
|
+
:build_and, :build_or, :build_condition,
|
14
|
+
:translate, :to => :base
|
15
|
+
|
16
|
+
def initialize(object, params = {})
|
17
|
+
params ||= {}
|
18
|
+
@context = Context.for(object)
|
19
|
+
@base = Nodes::And.new(@context)
|
20
|
+
build(params.with_indifferent_access)
|
21
|
+
end
|
22
|
+
|
23
|
+
def result(opts = {})
|
24
|
+
@result ||= @context.evaluate(self, opts)
|
25
|
+
end
|
26
|
+
|
27
|
+
def build(params)
|
28
|
+
collapse_multiparameter_attributes!(params).each do |key, value|
|
29
|
+
case key
|
30
|
+
when 's', 'sorts'
|
31
|
+
send("#{key}=", value)
|
32
|
+
else
|
33
|
+
base.send("#{key}=", value) if base.attribute_method?(key)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def sorts=(args)
|
40
|
+
case args
|
41
|
+
when Array
|
42
|
+
args.each do |sort|
|
43
|
+
sort = Nodes::Sort.extract(@context, sort)
|
44
|
+
self.sorts << sort
|
45
|
+
end
|
46
|
+
when Hash
|
47
|
+
args.each do |index, attrs|
|
48
|
+
sort = Nodes::Sort.new(@context).build(attrs)
|
49
|
+
self.sorts << sort
|
50
|
+
end
|
51
|
+
else
|
52
|
+
self.sorts = [args]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
alias :s= :sorts=
|
56
|
+
|
57
|
+
def sorts
|
58
|
+
@sorts ||= []
|
59
|
+
end
|
60
|
+
alias :s :sorts
|
61
|
+
|
62
|
+
def build_sort(opts = {})
|
63
|
+
new_sort(opts).tap do |sort|
|
64
|
+
self.sorts << sort
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def new_sort(opts = {})
|
69
|
+
Nodes::Sort.new(@context).build(opts)
|
70
|
+
end
|
71
|
+
|
72
|
+
def respond_to?(method_id)
|
73
|
+
super or begin
|
74
|
+
method_name = method_id.to_s
|
75
|
+
writer = method_name.sub!(/\=$/, '')
|
76
|
+
base.attribute_method?(method_name) ? true : false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def method_missing(method_id, *args)
|
81
|
+
method_name = method_id.to_s
|
82
|
+
writer = method_name.sub!(/\=$/, '')
|
83
|
+
if base.attribute_method?(method_name)
|
84
|
+
base.send(method_id, *args)
|
85
|
+
else
|
86
|
+
super
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def collapse_multiparameter_attributes!(attrs)
|
93
|
+
attrs.keys.each do |k|
|
94
|
+
if k.include?("(")
|
95
|
+
real_attribute, position = k.split(/\(|\)/)
|
96
|
+
cast = %w(a s i).include?(position.last) ? position.last : nil
|
97
|
+
position = position.to_i - 1
|
98
|
+
value = attrs.delete(k)
|
99
|
+
attrs[real_attribute] ||= []
|
100
|
+
attrs[real_attribute][position] = if cast
|
101
|
+
(value.blank? && cast == 'i') ? nil : value.send("to_#{cast}")
|
102
|
+
else
|
103
|
+
value
|
104
|
+
end
|
105
|
+
elsif Hash === attrs[k]
|
106
|
+
collapse_multiparameter_attributes!(attrs[k])
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
attrs
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'locale', '*.yml')]
|
2
|
+
|
3
|
+
module Ransack
|
4
|
+
module Translate
|
5
|
+
def self.word(key, options = {})
|
6
|
+
I18n.translate(:"ransack.#{key}", :default => key.to_s)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.predicate(key, options = {})
|
10
|
+
I18n.translate(:"ransack.predicates.#{key}", :default => key.to_s)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.attribute(key, options = {})
|
14
|
+
unless context = options.delete(:context)
|
15
|
+
raise ArgumentError, "A context is required to translate associations"
|
16
|
+
end
|
17
|
+
|
18
|
+
original_name = key.to_s
|
19
|
+
base_class = context.klass
|
20
|
+
base_ancestors = base_class.ancestors.select { |x| x.respond_to?(:model_name) }
|
21
|
+
predicate = Ransack::Configuration.predicate_keys.detect {|p| original_name.match(/_#{p}$/)}
|
22
|
+
attributes_str = original_name.sub(/_#{predicate}$/, '')
|
23
|
+
attribute_names = attributes_str.split(/_and_|_or_/)
|
24
|
+
combinator = attributes_str.match(/_and_/) ? :and : :or
|
25
|
+
defaults = base_ancestors.map do |klass|
|
26
|
+
:"ransack.attributes.#{klass.model_name.underscore}.#{original_name}"
|
27
|
+
end
|
28
|
+
|
29
|
+
translated_names = attribute_names.map do |attr|
|
30
|
+
attribute_name(context, attr, options[:include_associations])
|
31
|
+
end
|
32
|
+
|
33
|
+
interpolations = {}
|
34
|
+
interpolations[:attributes] = translated_names.join(" #{Translate.word(combinator)} ")
|
35
|
+
|
36
|
+
if predicate
|
37
|
+
defaults << "%{attributes} %{predicate}"
|
38
|
+
interpolations[:predicate] = Translate.predicate(predicate)
|
39
|
+
else
|
40
|
+
defaults << "%{attributes}"
|
41
|
+
end
|
42
|
+
|
43
|
+
defaults << options.delete(:default) if options[:default]
|
44
|
+
options.reverse_merge! :count => 1, :default => defaults
|
45
|
+
I18n.translate(defaults.shift, options.merge(interpolations))
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.association(key, options = {})
|
49
|
+
unless context = options.delete(:context)
|
50
|
+
raise ArgumentError, "A context is required to translate associations"
|
51
|
+
end
|
52
|
+
|
53
|
+
defaults = key.blank? ? [:"#{context.klass.i18n_scope}.models.#{context.klass.model_name.underscore}"] : [:"ransack.associations.#{context.klass.model_name.underscore}.#{key}"]
|
54
|
+
defaults << context.traverse(key).model_name.human
|
55
|
+
options = {:count => 1, :default => defaults}
|
56
|
+
I18n.translate(defaults.shift, options)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def self.attribute_name(context, name, include_associations = nil)
|
62
|
+
assoc_path = context.association_path(name)
|
63
|
+
associated_class = context.traverse(assoc_path) if assoc_path.present?
|
64
|
+
attr_name = name.sub(/^#{assoc_path}_/, '')
|
65
|
+
interpolations = {}
|
66
|
+
interpolations[:attr_fallback_name] = I18n.translate(
|
67
|
+
(associated_class ?
|
68
|
+
:"ransack.attributes.#{associated_class.model_name.underscore}.#{attr_name}" :
|
69
|
+
:"ransack.attributes.#{context.klass.model_name.underscore}.#{attr_name}"
|
70
|
+
),
|
71
|
+
:default => [
|
72
|
+
(associated_class ?
|
73
|
+
:"#{associated_class.i18n_scope}.attributes.#{associated_class.model_name.underscore}.#{attr_name}" :
|
74
|
+
:"#{context.klass.i18n_scope}.attributes.#{context.klass.model_name.underscore}.#{attr_name}"
|
75
|
+
),
|
76
|
+
attr_name.humanize
|
77
|
+
]
|
78
|
+
)
|
79
|
+
defaults = [
|
80
|
+
:"ransack.attributes.#{context.klass.model_name.underscore}.#{name}"
|
81
|
+
]
|
82
|
+
if include_associations && associated_class
|
83
|
+
defaults << '%{association_name} %{attr_fallback_name}'
|
84
|
+
interpolations[:association_name] = association(assoc_path, :context => context)
|
85
|
+
else
|
86
|
+
defaults << '%{attr_fallback_name}'
|
87
|
+
end
|
88
|
+
options = {:count => 1, :default => defaults}
|
89
|
+
I18n.translate(defaults.shift, options.merge(interpolations))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/ransack.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "ransack/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "ransack"
|
7
|
+
s.version = Ransack::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Ernie Miller"]
|
10
|
+
s.email = ["ernie@metautonomo.us"]
|
11
|
+
s.homepage = "http://metautonomo.us/projects/ransack"
|
12
|
+
s.summary = %q{Object-based searching. Like MetaSearch, but this time, with a better name.}
|
13
|
+
s.description = %q{Not yet ready for public consumption.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "ransack"
|
16
|
+
|
17
|
+
s.add_dependency 'activerecord', '~> 3.1.0.alpha'
|
18
|
+
s.add_dependency 'activesupport', '~> 3.1.0.alpha'
|
19
|
+
s.add_dependency 'actionpack', '~> 3.1.0.alpha'
|
20
|
+
s.add_development_dependency 'rspec', '~> 2.5.0'
|
21
|
+
s.add_development_dependency 'machinist', '~> 1.0.6'
|
22
|
+
s.add_development_dependency 'faker', '~> 0.9.5'
|
23
|
+
s.add_development_dependency 'sqlite3', '~> 1.3.3'
|
24
|
+
|
25
|
+
s.files = `git ls-files`.split("\n")
|
26
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
27
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
28
|
+
s.require_paths = ["lib"]
|
29
|
+
end
|
data/spec/console.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Bundler.setup
|
2
|
+
require 'machinist/active_record'
|
3
|
+
require 'sham'
|
4
|
+
require 'faker'
|
5
|
+
|
6
|
+
Dir[File.expand_path('../../spec/{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
|
7
|
+
require f
|
8
|
+
end
|
9
|
+
|
10
|
+
Sham.define do
|
11
|
+
name { Faker::Name.name }
|
12
|
+
title { Faker::Lorem.sentence }
|
13
|
+
body { Faker::Lorem.paragraph }
|
14
|
+
salary {|index| 30000 + (index * 1000)}
|
15
|
+
tag_name { Faker::Lorem.words(3).join(' ') }
|
16
|
+
note { Faker::Lorem.words(7).join(' ') }
|
17
|
+
end
|
18
|
+
|
19
|
+
Schema.create
|
20
|
+
|
21
|
+
require 'ransack'
|
22
|
+
|
data/spec/playground.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
$VERBOSE = false
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.setup
|
4
|
+
require 'machinist/active_record'
|
5
|
+
require 'sham'
|
6
|
+
require 'faker'
|
7
|
+
|
8
|
+
Dir[File.expand_path('../../spec/{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
|
9
|
+
require f
|
10
|
+
end
|
11
|
+
|
12
|
+
Sham.define do
|
13
|
+
name { Faker::Name.name }
|
14
|
+
title { Faker::Lorem.sentence }
|
15
|
+
body { Faker::Lorem.paragraph }
|
16
|
+
salary {|index| 30000 + (index * 1000)}
|
17
|
+
tag_name { Faker::Lorem.words(3).join(' ') }
|
18
|
+
note { Faker::Lorem.words(7).join(' ') }
|
19
|
+
end
|
20
|
+
|
21
|
+
Schema.create
|
22
|
+
|
23
|
+
require 'ransack'
|
24
|
+
|
25
|
+
Article.joins{person.comments}.where{person.comments.body =~ '%hello%'}.to_sql
|
26
|
+
# => "SELECT \"articles\".* FROM \"articles\" INNER JOIN \"people\" ON \"people\".\"id\" = \"articles\".\"person_id\" INNER JOIN \"comments\" ON \"comments\".\"person_id\" = \"people\".\"id\" WHERE \"comments\".\"body\" LIKE '%hello%'"
|
27
|
+
|
28
|
+
Person.where{(id + 1) == 2}.first
|
29
|
+
# => #<Person id: 1, parent_id: nil, name: "Aric Smith", salary: 31000>
|
30
|
+
|
31
|
+
Person.where{(salary - 40000) < 0}.to_sql
|
32
|
+
# => "SELECT \"people\".* FROM \"people\" WHERE \"people\".\"salary\" - 40000 < 0"
|
33
|
+
|
34
|
+
p = Person.select{[id, name, salary, (salary + 1000).as('salary_after_increase')]}.first
|
35
|
+
# => #<Person id: 1, name: "Aric Smith", salary: 31000>
|
36
|
+
|
37
|
+
p.salary_after_increase # =>
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Ransack
|
4
|
+
module Adapters
|
5
|
+
module ActiveRecord
|
6
|
+
describe Base do
|
7
|
+
|
8
|
+
it 'adds a ransack method to ActiveRecord::Base' do
|
9
|
+
::ActiveRecord::Base.should respond_to :ransack
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'aliases the method to search if available' do
|
13
|
+
::ActiveRecord::Base.should respond_to :search
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#search' do
|
17
|
+
before do
|
18
|
+
@s = Person.search
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'creates a search with Relation as its object' do
|
22
|
+
@s.should be_a Search
|
23
|
+
@s.object.should be_an ::ActiveRecord::Relation
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Ransack
|
4
|
+
module Adapters
|
5
|
+
module ActiveRecord
|
6
|
+
describe Context do
|
7
|
+
before do
|
8
|
+
@c = Context.new(Person)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'contextualizes strings to attributes' do
|
12
|
+
attribute = @c.contextualize 'children_children_parent_name'
|
13
|
+
attribute.should be_a Arel::Attributes::Attribute
|
14
|
+
attribute.name.should eq 'name'
|
15
|
+
attribute.relation.table_alias.should eq 'parents_people'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'builds new associations if not yet built' do
|
19
|
+
attribute = @c.contextualize 'children_articles_title'
|
20
|
+
attribute.should be_a Arel::Attributes::Attribute
|
21
|
+
attribute.name.should eq 'title'
|
22
|
+
attribute.relation.name.should eq 'articles'
|
23
|
+
attribute.relation.table_alias.should be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Ransack
|
4
|
+
module Helpers
|
5
|
+
describe FormBuilder do
|
6
|
+
|
7
|
+
router = ActionDispatch::Routing::RouteSet.new
|
8
|
+
router.draw do
|
9
|
+
resources :people
|
10
|
+
match ':controller(/:action(/:id(.:format)))'
|
11
|
+
end
|
12
|
+
|
13
|
+
include router.url_helpers
|
14
|
+
|
15
|
+
# FIXME: figure out a cleaner way to get this behavior
|
16
|
+
before do
|
17
|
+
@controller = ActionView::TestCase::TestController.new
|
18
|
+
@controller.instance_variable_set(:@_routes, router)
|
19
|
+
@controller.class_eval do
|
20
|
+
include router.url_helpers
|
21
|
+
end
|
22
|
+
|
23
|
+
@s = Person.search
|
24
|
+
@controller.view_context.search_form_for @s do |f|
|
25
|
+
@f = f
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'selects previously-entered time values with datetime_select' do
|
30
|
+
@s.created_at_eq = [2011, 1, 2, 3, 4, 5]
|
31
|
+
html = @f.datetime_select :created_at_eq
|
32
|
+
[2011, 1, 2, 3, 4, 5].each do |val|
|
33
|
+
html.should match /<option selected="selected" value="#{val}">#{val}<\/option>/o
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|