ransack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|