ransack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +11 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +5 -0
  5. data/Rakefile +19 -0
  6. data/lib/ransack.rb +24 -0
  7. data/lib/ransack/adapters/active_record.rb +2 -0
  8. data/lib/ransack/adapters/active_record/base.rb +17 -0
  9. data/lib/ransack/adapters/active_record/context.rb +153 -0
  10. data/lib/ransack/configuration.rb +39 -0
  11. data/lib/ransack/constants.rb +23 -0
  12. data/lib/ransack/context.rb +152 -0
  13. data/lib/ransack/helpers.rb +2 -0
  14. data/lib/ransack/helpers/form_builder.rb +172 -0
  15. data/lib/ransack/helpers/form_helper.rb +27 -0
  16. data/lib/ransack/locale/en.yml +67 -0
  17. data/lib/ransack/naming.rb +53 -0
  18. data/lib/ransack/nodes.rb +7 -0
  19. data/lib/ransack/nodes/and.rb +8 -0
  20. data/lib/ransack/nodes/attribute.rb +36 -0
  21. data/lib/ransack/nodes/condition.rb +209 -0
  22. data/lib/ransack/nodes/grouping.rb +207 -0
  23. data/lib/ransack/nodes/node.rb +34 -0
  24. data/lib/ransack/nodes/or.rb +8 -0
  25. data/lib/ransack/nodes/sort.rb +39 -0
  26. data/lib/ransack/nodes/value.rb +120 -0
  27. data/lib/ransack/predicate.rb +57 -0
  28. data/lib/ransack/search.rb +114 -0
  29. data/lib/ransack/translate.rb +92 -0
  30. data/lib/ransack/version.rb +3 -0
  31. data/ransack.gemspec +29 -0
  32. data/spec/blueprints/articles.rb +5 -0
  33. data/spec/blueprints/comments.rb +5 -0
  34. data/spec/blueprints/notes.rb +3 -0
  35. data/spec/blueprints/people.rb +4 -0
  36. data/spec/blueprints/tags.rb +3 -0
  37. data/spec/console.rb +22 -0
  38. data/spec/helpers/ransack_helper.rb +2 -0
  39. data/spec/playground.rb +37 -0
  40. data/spec/ransack/adapters/active_record/base_spec.rb +30 -0
  41. data/spec/ransack/adapters/active_record/context_spec.rb +29 -0
  42. data/spec/ransack/configuration_spec.rb +11 -0
  43. data/spec/ransack/helpers/form_builder_spec.rb +39 -0
  44. data/spec/ransack/nodes/compound_condition_spec.rb +0 -0
  45. data/spec/ransack/nodes/condition_spec.rb +0 -0
  46. data/spec/ransack/nodes/grouping_spec.rb +13 -0
  47. data/spec/ransack/predicate_spec.rb +25 -0
  48. data/spec/ransack/search_spec.rb +182 -0
  49. data/spec/spec_helper.rb +28 -0
  50. data/spec/support/schema.rb +102 -0
  51. 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
@@ -0,0 +1,3 @@
1
+ module Ransack
2
+ VERSION = "0.1.0"
3
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ Article.blueprint do
2
+ person
3
+ title
4
+ body
5
+ end
@@ -0,0 +1,5 @@
1
+ Comment.blueprint do
2
+ article
3
+ person
4
+ body
5
+ end
@@ -0,0 +1,3 @@
1
+ Note.blueprint do
2
+ note
3
+ end
@@ -0,0 +1,4 @@
1
+ Person.blueprint do
2
+ name
3
+ salary
4
+ end
@@ -0,0 +1,3 @@
1
+ Tag.blueprint do
2
+ name { Sham.tag_name }
3
+ end
@@ -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
+
@@ -0,0 +1,2 @@
1
+ module RansackHelper
2
+ end
@@ -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,11 @@
1
+ require 'spec_helper'
2
+
3
+ module Ransack
4
+ describe Configuration do
5
+ it 'yields self on configure' do
6
+ Ransack.configure do
7
+ self.should eq Ransack::Configuration
8
+ end
9
+ end
10
+ end
11
+ 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