searchlogic-heroku 2.4.19

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/.gitignore +7 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +308 -0
  4. data/Rakefile +35 -0
  5. data/VERSION.yml +5 -0
  6. data/init.rb +1 -0
  7. data/lib/searchlogic.rb +56 -0
  8. data/lib/searchlogic/active_record/association_proxy.rb +19 -0
  9. data/lib/searchlogic/active_record/consistency.rb +49 -0
  10. data/lib/searchlogic/active_record/named_scope_tools.rb +101 -0
  11. data/lib/searchlogic/core_ext/object.rb +43 -0
  12. data/lib/searchlogic/core_ext/proc.rb +17 -0
  13. data/lib/searchlogic/named_scopes/alias_scope.rb +67 -0
  14. data/lib/searchlogic/named_scopes/association_conditions.rb +131 -0
  15. data/lib/searchlogic/named_scopes/association_ordering.rb +44 -0
  16. data/lib/searchlogic/named_scopes/conditions.rb +226 -0
  17. data/lib/searchlogic/named_scopes/or_conditions.rb +141 -0
  18. data/lib/searchlogic/named_scopes/ordering.rb +48 -0
  19. data/lib/searchlogic/rails_helpers.rb +79 -0
  20. data/lib/searchlogic/search.rb +251 -0
  21. data/rails/init.rb +1 -0
  22. data/searchlogic.gemspec +89 -0
  23. data/spec/searchlogic/active_record/association_proxy_spec.rb +23 -0
  24. data/spec/searchlogic/active_record/consistency_spec.rb +28 -0
  25. data/spec/searchlogic/core_ext/object_spec.rb +9 -0
  26. data/spec/searchlogic/core_ext/proc_spec.rb +8 -0
  27. data/spec/searchlogic/named_scopes/alias_scope_spec.rb +23 -0
  28. data/spec/searchlogic/named_scopes/association_conditions_spec.rb +198 -0
  29. data/spec/searchlogic/named_scopes/association_ordering_spec.rb +27 -0
  30. data/spec/searchlogic/named_scopes/conditions_spec.rb +319 -0
  31. data/spec/searchlogic/named_scopes/or_conditions_spec.rb +66 -0
  32. data/spec/searchlogic/named_scopes/ordering_spec.rb +34 -0
  33. data/spec/searchlogic/search_spec.rb +459 -0
  34. data/spec/spec_helper.rb +132 -0
  35. metadata +128 -0
@@ -0,0 +1,251 @@
1
+ module Searchlogic
2
+ # A class that acts like a model, creates attr_accessors for named_scopes, and then
3
+ # chains together everything when an "action" method is called. It basically makes
4
+ # implementing search forms in your application effortless:
5
+ #
6
+ # search = User.search
7
+ # search.username_like = "bjohnson"
8
+ # search.all
9
+ #
10
+ # Is equivalent to:
11
+ #
12
+ # User.search(:username_like => "bjohnson").all
13
+ #
14
+ # Is equivalent to:
15
+ #
16
+ # User.username_like("bjohnson").all
17
+ class Search
18
+ # Responsible for adding a "search" method into your models.
19
+ module Implementation
20
+ # Additional method, gets aliased as "search" if that method
21
+ # is available. A lot of other libraries like to use "search"
22
+ # as well, so if you have a conflict like this, you can use
23
+ # this method directly.
24
+ def searchlogic(conditions = {})
25
+ Search.new(self, scope(:find), conditions)
26
+ end
27
+ end
28
+
29
+ # Is an invalid condition is used this error will be raised. Ex:
30
+ #
31
+ # User.search(:unkown => true)
32
+ #
33
+ # Where unknown is not a valid named scope for the User model.
34
+ class UnknownConditionError < StandardError
35
+ def initialize(condition)
36
+ msg = "The #{condition} is not a valid condition. You may only use conditions that map to a named scope"
37
+ super(msg)
38
+ end
39
+ end
40
+
41
+ attr_accessor :klass, :current_scope, :conditions
42
+ undef :id if respond_to?(:id)
43
+
44
+ # Creates a new search object for the given class. Ex:
45
+ #
46
+ # Searchlogic::Search.new(User, {}, {:username_like => "bjohnson"})
47
+ def initialize(klass, current_scope, conditions = {})
48
+ self.klass = klass
49
+ self.current_scope = current_scope
50
+ @conditions ||= {}
51
+ self.conditions = conditions if conditions.is_a?(Hash)
52
+ end
53
+
54
+ def clone
55
+ self.class.new(klass, current_scope && current_scope.clone, conditions.clone)
56
+ end
57
+
58
+ # Returns a hash of the current conditions set.
59
+ def conditions
60
+ mass_conditions.clone.merge(@conditions)
61
+ end
62
+
63
+ # Accepts a hash of conditions.
64
+ def conditions=(values)
65
+ values.each do |condition, value|
66
+ # if a condition name ends with "(1i)", assume it's date / datetime
67
+ if condition =~ /(.*)\(1i\)$/
68
+ date_scope_name = $1
69
+ date_parts = (1..6).to_a.map do |idx|
70
+ values.delete("#{ date_scope_name }(#{ idx }i)")
71
+ end.reject{|s| s.blank? }.map{|s| s.to_i }
72
+
73
+ # did we get enough info to build a time?
74
+ if date_parts.length >= 3
75
+ values[date_scope_name] = Time.zone.local(*date_parts)
76
+ end
77
+ end
78
+ end
79
+
80
+ values.each do |condition, value|
81
+ mass_conditions[condition.to_sym] = value
82
+ value.delete_if { |v| ignore_value?(v) } if value.is_a?(Array)
83
+ next if ignore_value?(value)
84
+ send("#{condition}=", value)
85
+ end
86
+ end
87
+
88
+ # Delete a condition from the search. Since conditions map to named scopes,
89
+ # if a named scope accepts a parameter there is no way to actually delete
90
+ # the scope if you do not want it anymore. A nil value might be meaningful
91
+ # to that scope.
92
+ def delete(*names)
93
+ names.each do |name|
94
+ @conditions.delete(name.to_sym)
95
+ mass_conditions.delete(name)
96
+ end
97
+ self
98
+ end
99
+
100
+ # Returns the column we are currently ordering by
101
+ def ordering_by
102
+ order && order.to_s.gsub(/^(ascend|descend)_by_/, '')
103
+ end
104
+
105
+ private
106
+ def method_missing(name, *args, &block)
107
+ condition_name = condition_name(name)
108
+ scope_name = scope_name(condition_name)
109
+
110
+ if setter?(name)
111
+ if scope?(scope_name)
112
+ if args.size == 1
113
+ write_condition(
114
+ condition_name,
115
+ type_cast(
116
+ args.first,
117
+ cast_type(scope_name),
118
+ scope_options(scope_name).respond_to?(:searchlogic_options) ? scope_options(scope_name).searchlogic_options : {}
119
+ )
120
+ )
121
+ else
122
+ write_condition(condition_name, args)
123
+ end
124
+ else
125
+ raise UnknownConditionError.new(condition_name)
126
+ end
127
+ elsif scope?(scope_name) && args.size <= 1
128
+ if args.size == 0
129
+ read_condition(condition_name)
130
+ else
131
+ send("#{condition_name}=", *args)
132
+ self
133
+ end
134
+ else
135
+ scope = conditions_array.inject(klass.scoped(current_scope) || {}) do |scope, condition|
136
+ scope_name, value = condition
137
+ scope_name = normalize_scope_name(scope_name)
138
+ klass.send(scope_name, value) if !klass.respond_to?(scope_name)
139
+ arity = klass.named_scope_arity(scope_name)
140
+
141
+ if !arity || arity == 0
142
+ if value == true
143
+ scope.send(scope_name)
144
+ else
145
+ scope
146
+ end
147
+ elsif arity == -1
148
+ scope.send(scope_name, *(value.is_a?(Array) ? value : [value]))
149
+ else
150
+ scope.send(scope_name, value)
151
+ end
152
+ end
153
+ scope.send(name, *args, &block)
154
+ end
155
+ end
156
+
157
+ # This is here as a hook to allow people to modify the order in which the conditions are called, for whatever reason.
158
+ def conditions_array
159
+ @conditions.to_a
160
+ end
161
+
162
+ def normalize_scope_name(scope_name)
163
+ case
164
+ when klass.scopes.key?(scope_name.to_sym) then scope_name.to_sym
165
+ when klass.column_names.include?(scope_name.to_s) then "#{scope_name}_equals".to_sym
166
+ else scope_name.to_sym
167
+ end
168
+ end
169
+
170
+ def setter?(name)
171
+ !(name.to_s =~ /=$/).nil?
172
+ end
173
+
174
+ def condition_name(name)
175
+ condition = name.to_s.match(/(\w+)=?$/)
176
+ condition ? condition[1].to_sym : nil
177
+ end
178
+
179
+ def write_condition(name, value)
180
+ @conditions[name] = value
181
+ end
182
+
183
+ def read_condition(name)
184
+ @conditions[name]
185
+ end
186
+
187
+ def mass_conditions
188
+ @mass_conditions ||= {}
189
+ end
190
+
191
+ def scope_name(condition_name)
192
+ condition_name && normalize_scope_name(condition_name)
193
+ end
194
+
195
+ def scope?(scope_name)
196
+ klass.scopes.key?(scope_name) || klass.condition?(scope_name)
197
+ end
198
+
199
+ def scope_options(name)
200
+ klass.send(name, nil) if !klass.respond_to?(name) # We need to set up the named scope if it doesn't exist, so we can get a value for named_scope_options
201
+ klass.named_scope_options(name)
202
+ end
203
+
204
+ def cast_type(name)
205
+ named_scope_options = scope_options(name)
206
+ arity = klass.named_scope_arity(name)
207
+ if !arity || arity == 0
208
+ :boolean
209
+ else
210
+ named_scope_options.respond_to?(:searchlogic_options) ? named_scope_options.searchlogic_options[:type] : :string
211
+ end
212
+ end
213
+
214
+ def type_cast(value, type, options = {})
215
+ case value
216
+ when Array
217
+ value.collect { |v| type_cast(v, type) }
218
+ when Range
219
+ Range.new(type_cast(value.first, type), type_cast(value.last, type))
220
+ else
221
+ # Let's leverage ActiveRecord's type casting, so that casting is consistent
222
+ # with the other models.
223
+ column_for_type_cast = ::ActiveRecord::ConnectionAdapters::Column.new("", nil)
224
+ column_for_type_cast.instance_variable_set(:@type, type)
225
+ casted_value = column_for_type_cast.type_cast(value)
226
+
227
+ if Time.zone && casted_value.is_a?(Time)
228
+ if value.is_a?(String)
229
+ if options[:skip_conversion]
230
+ casted_value.utc
231
+ else
232
+ (casted_value + (Time.zone.utc_offset * -1)).in_time_zone
233
+ end
234
+ else
235
+ if options[:skip_conversion]
236
+ casted_value.utc
237
+ else
238
+ casted_value.in_time_zone
239
+ end
240
+ end
241
+ else
242
+ casted_value
243
+ end
244
+ end
245
+ end
246
+
247
+ def ignore_value?(value)
248
+ (value.is_a?(String) && value.blank?) || (value.is_a?(Array) && value.empty?)
249
+ end
250
+ end
251
+ end
@@ -0,0 +1 @@
1
+ require "searchlogic"
@@ -0,0 +1,89 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{searchlogic}
8
+ s.version = "2.4.19.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Ben Johnson of Binary Logic", "William Yeung @ Tofugear"]
12
+ s.date = %q{2010-07-26}
13
+ s.description = %q{Searchlogic makes using ActiveRecord named scopes easier and less repetitive. Patch to fix heroku form_for issue}
14
+ s.email = %q{bjohnson@binarylogic.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".gitignore",
21
+ "LICENSE",
22
+ "README.rdoc",
23
+ "Rakefile",
24
+ "VERSION.yml",
25
+ "init.rb",
26
+ "lib/searchlogic.rb",
27
+ "lib/searchlogic/active_record/association_proxy.rb",
28
+ "lib/searchlogic/active_record/consistency.rb",
29
+ "lib/searchlogic/active_record/named_scope_tools.rb",
30
+ "lib/searchlogic/core_ext/object.rb",
31
+ "lib/searchlogic/core_ext/proc.rb",
32
+ "lib/searchlogic/named_scopes/alias_scope.rb",
33
+ "lib/searchlogic/named_scopes/association_conditions.rb",
34
+ "lib/searchlogic/named_scopes/association_ordering.rb",
35
+ "lib/searchlogic/named_scopes/conditions.rb",
36
+ "lib/searchlogic/named_scopes/or_conditions.rb",
37
+ "lib/searchlogic/named_scopes/ordering.rb",
38
+ "lib/searchlogic/rails_helpers.rb",
39
+ "lib/searchlogic/search.rb",
40
+ "rails/init.rb",
41
+ "searchlogic.gemspec",
42
+ "spec/searchlogic/active_record/association_proxy_spec.rb",
43
+ "spec/searchlogic/active_record/consistency_spec.rb",
44
+ "spec/searchlogic/core_ext/object_spec.rb",
45
+ "spec/searchlogic/core_ext/proc_spec.rb",
46
+ "spec/searchlogic/named_scopes/alias_scope_spec.rb",
47
+ "spec/searchlogic/named_scopes/association_conditions_spec.rb",
48
+ "spec/searchlogic/named_scopes/association_ordering_spec.rb",
49
+ "spec/searchlogic/named_scopes/conditions_spec.rb",
50
+ "spec/searchlogic/named_scopes/or_conditions_spec.rb",
51
+ "spec/searchlogic/named_scopes/ordering_spec.rb",
52
+ "spec/searchlogic/search_spec.rb",
53
+ "spec/spec_helper.rb"
54
+ ]
55
+ s.homepage = %q{http://github.com/goodwill/searchlogic}
56
+ s.rdoc_options = ["--charset=UTF-8"]
57
+ s.require_paths = ["lib"]
58
+ s.rubyforge_project = %q{searchlogic}
59
+ s.rubygems_version = %q{1.3.6}
60
+ s.summary = %q{Searchlogic makes using ActiveRecord named scopes easier and less repetitive.}
61
+ s.test_files = [
62
+ "spec/searchlogic/active_record/association_proxy_spec.rb",
63
+ "spec/searchlogic/active_record/consistency_spec.rb",
64
+ "spec/searchlogic/core_ext/object_spec.rb",
65
+ "spec/searchlogic/core_ext/proc_spec.rb",
66
+ "spec/searchlogic/named_scopes/alias_scope_spec.rb",
67
+ "spec/searchlogic/named_scopes/association_conditions_spec.rb",
68
+ "spec/searchlogic/named_scopes/association_ordering_spec.rb",
69
+ "spec/searchlogic/named_scopes/conditions_spec.rb",
70
+ "spec/searchlogic/named_scopes/or_conditions_spec.rb",
71
+ "spec/searchlogic/named_scopes/ordering_spec.rb",
72
+ "spec/searchlogic/search_spec.rb",
73
+ "spec/spec_helper.rb"
74
+ ]
75
+
76
+ if s.respond_to? :specification_version then
77
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
78
+ s.specification_version = 3
79
+
80
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
81
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.0.0"])
82
+ else
83
+ s.add_dependency(%q<activerecord>, [">= 2.0.0"])
84
+ end
85
+ else
86
+ s.add_dependency(%q<activerecord>, [">= 2.0.0"])
87
+ end
88
+ end
89
+
@@ -0,0 +1,23 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+ describe "Searchlogic::ActiveRecord::AssociationProxy" do
4
+ it "should call location conditions" do
5
+ company = Company.create
6
+ user = company.users.create(:username => "bjohnson")
7
+ company.users.send(:username_like, "bjohnson").should == [user]
8
+ end
9
+
10
+ it "should call ordering conditions" do
11
+ company = Company.create
12
+ user = company.users.create(:username => "bjohnson")
13
+ company.users.send(:ascend_by_username).should == [user]
14
+ end
15
+
16
+ it "should call 'or' conditions" do
17
+ company = Company.create
18
+ user = company.users.create(:username => "bjohnson")
19
+ company.users.send(:username_or_some_type_id_like, "bjohnson").should == [user]
20
+ end
21
+
22
+ end
23
+
@@ -0,0 +1,28 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+ describe Searchlogic::ActiveRecord::Consistency do
4
+ it "should merge joins with consistent conditions" do
5
+ user_group = UserGroup.create
6
+ user_group.users.user_groups_name_like("name").user_groups_id_gt(10).scope(:find)[:joins].should == [
7
+ "INNER JOIN \"user_groups_users\" ON \"user_groups_users\".user_id = \"users\".id",
8
+ "INNER JOIN \"user_groups\" ON \"user_groups\".id = \"user_groups_users\".user_group_id"
9
+ ]
10
+ end
11
+
12
+ it "should respect parenthesis when reordering conditions" do
13
+ joins = [
14
+ "INNER JOIN \"table\" ON (\"b\".user_id = \"a\".id)",
15
+ "INNER JOIN \"table\" ON (\"b\".id = \"a\".user_group_id)"
16
+ ]
17
+ ActiveRecord::Base.send(:merge_joins, joins).should == [
18
+ "INNER JOIN \"table\" ON \"a\".id = \"b\".user_id",
19
+ "INNER JOIN \"table\" ON \"a\".user_group_id = \"b\".id"
20
+ ]
21
+ end
22
+
23
+ it "shuold not convert joins to strings when delegating via associations" do
24
+ User.alias_scope :has_id_gt, lambda { User.id_gt(10).has_name.orders_id_gt(10) }
25
+ User.alias_scope :has_name, lambda { User.orders_created_at_after(Time.now).name_equals("ben").username_equals("ben") }
26
+ Company.users_has_id_gt.proxy_options[:joins].should == {:users=>[:orders]}
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+ describe Searchlogic::CoreExt::Object do
4
+ it "should accept and pass the argument to the searchlogic_options" do
5
+ proc = searchlogic_lambda(:integer, :test => :value) {}
6
+ proc.searchlogic_options[:type].should == :integer
7
+ proc.searchlogic_options[:test].should == :value
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+ describe Searchlogic::CoreExt::Proc do
4
+ it "should have a searchlogic_options accessor" do
5
+ p = Proc.new {}
6
+ p.searchlogic_options[:type] = :integer
7
+ end
8
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+ describe Searchlogic::NamedScopes::AliasScope do
4
+ before(:each) do
5
+ User.alias_scope :username_has, lambda { |value| User.username_like(value) }
6
+ end
7
+
8
+ it "should allow alias scopes" do
9
+ User.create(:username => "bjohnson")
10
+ User.create(:username => "thunt")
11
+ User.username_has("bjohnson").all.should == User.find_all_by_username("bjohnson")
12
+ end
13
+
14
+ it "should allow alias scopes from the search object" do
15
+ search = User.search
16
+ search.username_has = "bjohnson"
17
+ search.username_has.should == "bjohnson"
18
+ end
19
+
20
+ it "should inherit alias scopes from superclasses" do
21
+ Class.new(User).alias_scope?("username_has").should be_true
22
+ end
23
+ end
@@ -0,0 +1,198 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+ describe Searchlogic::NamedScopes::AssociationConditions do
4
+ it "should create a named scope" do
5
+ Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => :users)
6
+ end
7
+
8
+ it "should create a deep named scope" do
9
+ Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => {:users => :orders})
10
+ end
11
+
12
+ it "should allow the use of foreign pre-existing named scopes" do
13
+ User.named_scope :uname, lambda { |value| {:conditions => ["users.username = ?", value]} }
14
+ Company.users_uname("bjohnson").proxy_options.should == User.uname("bjohnson").proxy_options.merge(:joins => :users)
15
+ end
16
+
17
+ it "should allow the use of deep foreign pre-existing named scopes" do
18
+ pending
19
+ Order.named_scope :big_id, :conditions => "orders.id > 100"
20
+ Company.users_orders_big_id.proxy_options.should == Order.big_id.proxy_options.merge(:joins => {:users => :orders})
21
+ end
22
+
23
+ it "should allow the use of foreign pre-existing alias scopes" do
24
+ User.alias_scope :username_has, lambda { |value| User.username_like(value) }
25
+ Company.users_username_has("bjohnson").proxy_options.should == User.username_has("bjohnson").proxy_options.merge(:joins => :users)
26
+ end
27
+
28
+ it "should not raise errors for scopes that don't return anything" do
29
+ User.alias_scope :blank_scope, lambda { |value| }
30
+ Company.users_blank_scope("bjohnson").proxy_options.should == {:joins => :users}
31
+ end
32
+
33
+ it "should ignore polymorphic associations" do
34
+ lambda { Fee.owner_created_at_gt(Time.now) }.should raise_error(NoMethodError)
35
+ end
36
+
37
+ it "should not allow named scopes on non existent association columns" do
38
+ lambda { User.users_whatever_like("bjohnson") }.should raise_error(NoMethodError)
39
+ end
40
+
41
+ it "should not allow named scopes on non existent deep association columns" do
42
+ lambda { User.users_orders_whatever_like("bjohnson") }.should raise_error(NoMethodError)
43
+ end
44
+
45
+ it "should allow named scopes to be called multiple times and reflect the value passed" do
46
+ Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => :users)
47
+ Company.users_username_like("thunt").proxy_options.should == User.username_like("thunt").proxy_options.merge(:joins => :users)
48
+ end
49
+
50
+ it "should allow deep named scopes to be called multiple times and reflect the value passed" do
51
+ Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => {:users => :orders})
52
+ Company.users_orders_total_greater_than(20).proxy_options.should == Order.total_greater_than(20).proxy_options.merge(:joins => {:users => :orders})
53
+ end
54
+
55
+ it "should have an arity of 1 if the underlying scope has an arity of 1" do
56
+ Company.users_orders_total_greater_than(10)
57
+ Company.named_scope_arity("users_orders_total_greater_than").should == Order.named_scope_arity("total_greater_than")
58
+ end
59
+
60
+ it "should have an arity of nil if the underlying scope has an arity of nil" do
61
+ Company.users_orders_total_null
62
+ Company.named_scope_arity("users_orders_total_null").should == Order.named_scope_arity("total_null")
63
+ end
64
+
65
+ it "should have an arity of -1 if the underlying scope has an arity of -1" do
66
+ Company.users_id_equals_any
67
+ Company.named_scope_arity("users_id_equals_any").should == User.named_scope_arity("id_equals_any")
68
+ end
69
+
70
+ it "should allow aliases" do
71
+ Company.users_username_contains("bjohnson").proxy_options.should == User.username_contains("bjohnson").proxy_options.merge(:joins => :users)
72
+ end
73
+
74
+ it "should allow deep aliases" do
75
+ Company.users_orders_total_gt(10).proxy_options.should == Order.total_gt(10).proxy_options.merge(:joins => {:users => :orders})
76
+ end
77
+
78
+ it "should include optional associations" do
79
+ pending # this is a problem with using inner joins and left outer joins
80
+ Company.create
81
+ company = Company.create
82
+ user = company.users.create
83
+ order = user.orders.create(:total => 20, :taxes => 3)
84
+ Company.ascend_by_users_orders_total.all.should == Company.all
85
+ end
86
+
87
+ it "should implement exclusive scoping" do
88
+ scope = Company.users_company_name_like("name").users_company_description_like("description")
89
+ scope.scope(:find)[:joins].should == [
90
+ "INNER JOIN \"users\" ON companies.id = users.company_id",
91
+ "INNER JOIN \"companies\" companies_users ON \"companies_users\".id = \"users\".company_id"
92
+ ]
93
+ lambda { scope.all }.should_not raise_error
94
+ end
95
+
96
+ it "should not create the same join twice" do
97
+ scope = Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total
98
+ scope.scope(:find)[:joins].should == [
99
+ "INNER JOIN \"users\" ON companies.id = users.company_id",
100
+ "INNER JOIN \"orders\" ON orders.user_id = users.id"
101
+ ]
102
+ lambda { scope.count }.should_not raise_error
103
+ end
104
+
105
+ it "should not create the same join twice when traveling through the duplicate join" do
106
+ scope = Company.users_username_like("bjohnson").users_orders_total_gt(100)
107
+ scope.scope(:find)[:joins].should == [
108
+ "INNER JOIN \"users\" ON companies.id = users.company_id",
109
+ "INNER JOIN \"orders\" ON orders.user_id = users.id"
110
+ ]
111
+ lambda { scope.count }.should_not raise_error
112
+ end
113
+
114
+ it "should not create the same join twice when traveling through the deep duplicate join" do
115
+ scope = Company.users_orders_total_gt(100).users_orders_line_items_price_gt(20)
116
+ scope.scope(:find)[:joins].should == [
117
+ "INNER JOIN \"users\" ON companies.id = users.company_id",
118
+ "INNER JOIN \"orders\" ON orders.user_id = users.id",
119
+ "INNER JOIN \"line_items\" ON line_items.order_id = orders.id"
120
+ ]
121
+ lambda { scope.all }.should_not raise_error
122
+ end
123
+
124
+ it "should allow the use of :include when a join was created" do
125
+ company = Company.create
126
+ user = company.users.create
127
+ order = user.orders.create(:total => 20, :taxes => 3)
128
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
129
+ end
130
+
131
+ it "should allow the use of deep :include when a join was created" do
132
+ company = Company.create
133
+ user = company.users.create
134
+ order = user.orders.create(:total => 20, :taxes => 3)
135
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
136
+ end
137
+
138
+ it "should allow the use of :include when traveling through the duplicate join" do
139
+ company = Company.create
140
+ user = company.users.create(:username => "bjohnson")
141
+ order = user.orders.create(:total => 20, :taxes => 3)
142
+ Company.users_username_like("bjohnson").users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
143
+ end
144
+
145
+ it "should allow the use of deep :include when traveling through the duplicate join" do
146
+ company = Company.create
147
+ user = company.users.create(:username => "bjohnson")
148
+ order = user.orders.create(:total => 20, :taxes => 3)
149
+ Company.users_orders_taxes_lt(50).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
150
+ end
151
+
152
+ it "should automatically add string joins if the association condition is using strings" do
153
+ User.named_scope(:orders_big_id, :joins => User.inner_joins(:orders))
154
+ Company.users_orders_big_id.proxy_options.should == {:joins=>[" INNER JOIN \"users\" ON users.company_id = companies.id ", " INNER JOIN \"orders\" ON orders.user_id = users.id "]}
155
+ end
156
+
157
+ it "should order the join statements ascending by the fieldnames so that we don't get double joins where the only difference is that the order of the fields is different" do
158
+ company = Company.create
159
+ user = company.users.create(:company_id => company.id)
160
+ company.users.company_id_eq(company.id).should == [user]
161
+ end
162
+
163
+ it "should sanitize the scope on a foreign model instead of passing the raw options back to the original" do
164
+ Company.named_scope(:users_count_10, :conditions => {:users_count => 10})
165
+ User.company_users_count_10.proxy_options.should == {:conditions => "\"companies\".\"users_count\" = 10", :joins => :company}
166
+ end
167
+
168
+ it "should delegate to polymorphic relationships" do
169
+ Audit.auditable_user_type_name_like("ben").proxy_options.should == {
170
+ :conditions => ["users.name LIKE ?", "%ben%"],
171
+ :joins => "INNER JOIN \"users\" ON \"users\".id = \"audits\".auditable_id AND \"audits\".auditable_type = 'User'"
172
+ }
173
+ end
174
+
175
+ it "should delegate to polymorphic relationships (with a lazy split on _type_)" do
176
+ Audit.auditable_user_type_some_type_id_like("ben").proxy_options.should == {
177
+ :conditions => ["users.some_type_id LIKE ?", "%ben%"],
178
+ :joins => "INNER JOIN \"users\" ON \"users\".id = \"audits\".auditable_id AND \"audits\".auditable_type = 'User'"
179
+ }
180
+ end
181
+
182
+ it "should deep delegate to polymorphic relationships" do
183
+ Audit.auditable_user_type_company_name_like("company").proxy_options.should == {
184
+ :conditions => ["companies.name LIKE ?", "%company%"],
185
+ :joins => ["INNER JOIN \"users\" ON \"users\".id = \"audits\".auditable_id AND \"audits\".auditable_type = 'User'", " INNER JOIN \"companies\" ON \"companies\".id = \"users\".company_id "]
186
+ }
187
+ end
188
+
189
+ it "should allow any on a has_many relationship" do
190
+ company1 = Company.create
191
+ user1 = company1.users.create
192
+ company2 = Company.create
193
+ user2 = company2.users.create
194
+ user3 = company2.users.create
195
+
196
+ Company.users_id_equals_any([user2.id, user3.id]).all(:select => "DISTINCT companies.*").should == [company2]
197
+ end
198
+ end