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.
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