binarylogic-searchlogic 2.0.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.
@@ -0,0 +1,215 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for columns.
4
+ module Conditions
5
+ COMPARISON_CONDITIONS = {
6
+ :equals => [:is, :eq],
7
+ :does_not_equal => [:not_equal_to, :is_not, :not, :ne],
8
+ :less_than => [:lt, :before],
9
+ :less_than_or_equal_to => [:lte],
10
+ :greater_than => [:gt, :after],
11
+ :greater_than_or_equal_to => [:gte],
12
+ }
13
+
14
+ WILDCARD_CONDITIONS = {
15
+ :like => [:contains, :includes],
16
+ :begins_with => [:bw],
17
+ :ends_with => [:ew],
18
+ }
19
+
20
+ BOOLEAN_CONDITIONS = {
21
+ :null => [:nil],
22
+ :empty => []
23
+ }
24
+
25
+ CONDITIONS = {}
26
+
27
+ COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases|
28
+ CONDITIONS[condition] = aliases
29
+ CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym }
30
+ CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym }
31
+ end
32
+
33
+ BOOLEAN_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases }
34
+
35
+ PRIMARY_CONDITIONS = CONDITIONS.keys
36
+ ALIAS_CONDITIONS = CONDITIONS.values.flatten
37
+
38
+ # Retrieves the options passed when creating the respective named scope. Ex:
39
+ #
40
+ # named_scope :whatever, :conditions => {:column => value}
41
+ #
42
+ # This method will return:
43
+ #
44
+ # :conditions => {:column => value}
45
+ #
46
+ # ActiveRecord hides this internally, so we have to try and pull it out with this
47
+ # method.
48
+ def named_scope_options(name)
49
+ key = primary_condition_name(name)
50
+
51
+ if key
52
+ eval("options", scopes[key])
53
+ else
54
+ nil
55
+ end
56
+ end
57
+
58
+ # The arity for a named scope's proc is important, because we use the arity
59
+ # to determine if the condition should be ignored when calling the search method.
60
+ # If the condition is false and the arity is 0, then we skip it all together. Ex:
61
+ #
62
+ # User.named_scope :age_is_4, :conditions => {:age => 4}
63
+ # User.search(:age_is_4 => false) == User.all
64
+ # User.search(:age_is_4 => true) == User.all(:conditions => {:age => 4})
65
+ #
66
+ # We also use it when trying to "copy" the underlying named scope for association
67
+ # conditions.
68
+ def named_scope_arity(name)
69
+ options = named_scope_options(name)
70
+ options.respond_to?(:arity) ? options.arity : nil
71
+ end
72
+
73
+ # Returns the primary condition for the given alias. Ex:
74
+ #
75
+ # primary_condition(:gt) => :greater_than
76
+ def primary_condition(alias_condition)
77
+ CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
78
+ end
79
+
80
+ # Returns the primary name for any condition on a column. You can pass it
81
+ # a primary condition, alias condition, etc, and it will return the proper
82
+ # primary condition name. This helps simply logic throughout Searchlogic. Ex:
83
+ #
84
+ # primary_condition_name(:id_gt) => :id_greater_than
85
+ # primary_condition_name(:id_greater_than) => :id_greater_than
86
+ def primary_condition_name(name)
87
+ if primary_condition?(name)
88
+ name.to_sym
89
+ elsif details = alias_condition_details(name)
90
+ "#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
91
+ else
92
+ nil
93
+ end
94
+ end
95
+
96
+ # Is the name of the method a valid condition that can be dynamically created?
97
+ def condition?(name)
98
+ primary_condition?(name) || alias_condition?(name)
99
+ end
100
+
101
+ # Is the name of the method a valid condition that can be dynamically created,
102
+ # AND is it a primary condition (not an alias). "greater_than" not "gt".
103
+ def primary_condition?(name)
104
+ !primary_condition_details(name).nil?
105
+ end
106
+
107
+ # Is the name of the method a valid condition that can be dynamically created,
108
+ # AND is it an alias condition. "gt" not "greater_than".
109
+ def alias_condition?(name)
110
+ !alias_condition_details(name).nil?
111
+ end
112
+
113
+ private
114
+ def method_missing(name, *args, &block)
115
+ if details = primary_condition_details(name)
116
+ create_primary_condition(details[:column], details[:condition])
117
+ send(name, *args)
118
+ elsif details = alias_condition_details(name)
119
+ create_alias_condition(details[:column], details[:condition], args)
120
+ send(name, *args)
121
+ else
122
+ super
123
+ end
124
+ end
125
+
126
+ def primary_condition_details(name)
127
+ if name.to_s =~ /^(#{column_names.join("|")})_(#{PRIMARY_CONDITIONS.join("|")})$/
128
+ {:column => $1, :condition => $2}
129
+ end
130
+ end
131
+
132
+ def create_primary_condition(column, condition)
133
+ column_type = columns_hash[column.to_s].type
134
+ scope_options = case condition.to_s
135
+ when /^equals/
136
+ scope_options(condition, column_type, "#{table_name}.#{column} = ?")
137
+ when /^does_not_equal/
138
+ scope_options(condition, column_type, "#{table_name}.#{column} != ?")
139
+ when /^less_than_or_equal_to/
140
+ scope_options(condition, column_type, "#{table_name}.#{column} <= ?")
141
+ when /^less_than/
142
+ scope_options(condition, column_type, "#{table_name}.#{column} < ?")
143
+ when /^greater_than_or_equal_to/
144
+ scope_options(condition, column_type, "#{table_name}.#{column} >= ?")
145
+ when /^greater_than/
146
+ scope_options(condition, column_type, "#{table_name}.#{column} > ?")
147
+ when /^like/
148
+ scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :like)
149
+ when /^begins_with/
150
+ scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :begins_with)
151
+ when /^ends_with/
152
+ scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :ends_with)
153
+ when "null"
154
+ {:conditions => "#{table_name}.#{column} IS NULL"}
155
+ when "empty"
156
+ {:conditions => "#{table_name}.#{column} = ''"}
157
+ end
158
+
159
+ named_scope("#{column}_#{condition}".to_sym, scope_options)
160
+ end
161
+
162
+ # This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions.
163
+ # Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as
164
+ # fast as possible, which is why I didn't do the case statement inside of the lambda.
165
+ def scope_options(condition, column_type, sql, value_modifier = nil)
166
+ case condition.to_s
167
+ when /_(any|all)$/
168
+ searchlogic_lambda(column_type) { |*values|
169
+ return {} if values.empty?
170
+ values = values.flatten
171
+
172
+ values_to_sub = nil
173
+ if value_modifier.nil?
174
+ values_to_sub = values
175
+ else
176
+ values_to_sub = values.collect { |value| value_with_modifier(value, value_modifier) }
177
+ end
178
+
179
+ join = $1 == "any" ? " OR " : " AND "
180
+ {:conditions => [values.collect { |value| sql }.join(join), *values_to_sub]}
181
+ }
182
+ else
183
+ searchlogic_lambda(column_type) { |value| {:conditions => [sql, value_with_modifier(value, value_modifier)]} }
184
+ end
185
+ end
186
+
187
+ def value_with_modifier(value, modifier)
188
+ case modifier
189
+ when :like
190
+ "%#{value}%"
191
+ when :begins_with
192
+ "#{value}%"
193
+ when :ends_with
194
+ "%#{value}"
195
+ else
196
+ value
197
+ end
198
+ end
199
+
200
+ def alias_condition_details(name)
201
+ if name.to_s =~ /^(#{column_names.join("|")})_(#{ALIAS_CONDITIONS.join("|")})$/
202
+ {:column => $1, :condition => $2}
203
+ end
204
+ end
205
+
206
+ def create_alias_condition(column, condition, args)
207
+ primary_condition = primary_condition(condition)
208
+ alias_name = "#{column}_#{condition}"
209
+ primary_name = "#{column}_#{primary_condition}"
210
+ send(primary_name, *args) # go back to method_missing and make sure we create the method
211
+ (class << self; self; end).class_eval { alias_method alias_name, primary_name }
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,53 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for orderin by columns.
4
+ module Ordering
5
+ def condition?(name) # :nodoc:
6
+ super || order_condition?(name)
7
+ end
8
+
9
+ def primary_condition_name(name) # :nodoc
10
+ if result = super
11
+ result
12
+ elsif order_condition?(name)
13
+ name.to_sym
14
+ else
15
+ nil
16
+ end
17
+ end
18
+
19
+ def order_condition?(name) # :nodoc:
20
+ !order_condition_details(name).nil?
21
+ end
22
+
23
+ private
24
+ def method_missing(name, *args, &block)
25
+ if name == :order
26
+ named_scope name, lambda { |scope_name|
27
+ return {} if !order_condition?(scope_name)
28
+ send(scope_name).proxy_options
29
+ }
30
+ send(name, *args)
31
+ elsif details = order_condition_details(name)
32
+ create_order_conditions(details[:column])
33
+ send(name, *args)
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def order_condition_details(name)
40
+ if name.to_s =~ /^(ascend|descend)_by_(\w+)$/
41
+ {:order_as => $1, :column => $2}
42
+ elsif name.to_s =~ /^order$/
43
+ {}
44
+ end
45
+ end
46
+
47
+ def create_order_conditions(column)
48
+ named_scope("ascend_by_#{column}".to_sym, {:order => "#{table_name}.#{column} ASC"})
49
+ named_scope("descend_by_#{column}".to_sym, {:order => "#{table_name}.#{column} DESC"})
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,63 @@
1
+ module Searchlogic
2
+ module RailsHelpers
3
+ # Creates a link that alternates between acending and descending. It basically
4
+ # alternates between calling 2 named scopes: "ascend_by_*" and "descend_by_*"
5
+ #
6
+ # By default Searchlogic gives you these named scopes for all of your columns, but
7
+ # if you wanted to create your own, it will work with those too.
8
+ #
9
+ # This helper accepts the following options:
10
+ #
11
+ # * <tt>:by</tt> - the name of the named scope. This helper will prepend this value with "ascend_by_" and "descend_by_"
12
+ # * <tt>:as</tt> - the text used in the link, defaults to whatever is passed to :by
13
+ # * <tt>:ascend_scope</tt> - what scope to call for ascending the data, defaults to "ascend_by_:by"
14
+ # * <tt>:descend_scope</tt> - what scope to call for descending the data, defaults to "descend_by_:by"
15
+ # * <tt>:params_scope</tt> - the name of the params key to scope the order condition by, defaults to :search
16
+ def order(search, options = {}, html_options = {})
17
+ options[:params_scope] ||= :search
18
+ options[:as] ||= options[:by].to_s.humanize
19
+ options[:ascend_scope] ||= "ascend_by_#{options[:by]}"
20
+ options[:descend_scope] ||= "descend_by_#{options[:by]}"
21
+ ascending = search.order.to_s == options[:ascend_scope]
22
+ new_scope = ascending ? options[:descend_scope] : options[:ascend_scope]
23
+ selected = [options[:ascend_scope], options[:descend_scope]].include?(search.order.to_s)
24
+ if selected
25
+ css_classes = html_options[:class] ? html_options[:class].split(" ") : []
26
+ if ascending
27
+ options[:as] = "&#9650;&nbsp;#{options[:as]}"
28
+ css_classes << "ascending"
29
+ else
30
+ options[:as] = "&#9660;&nbsp;#{options[:as]}"
31
+ css_classes << "descending"
32
+ end
33
+ html_options[:class] = css_classes.join(" ")
34
+ end
35
+ link_to options[:as], url_for(options[:params_scope] => {:order => new_scope}), html_options
36
+ end
37
+
38
+ # Automatically makes the form method :get if a Searchlogic::Search and sets
39
+ # the params scope to :search
40
+ def form_for(*args, &block)
41
+ if search_obj = args.find { |arg| arg.is_a?(Searchlogic::Search) }
42
+ options = args.extract_options!
43
+ options[:html] ||= {}
44
+ options[:html][:method] ||= :get
45
+ args.unshift(:search) if args.first == search_obj
46
+ args << options
47
+ end
48
+ super
49
+ end
50
+
51
+ # Automatically adds an "order" hidden field in your form to preserve how the data
52
+ # is being ordered.
53
+ def fields_for(*args, &block)
54
+ if search_obj = args.find { |arg| arg.is_a?(Searchlogic::Search) }
55
+ args.unshift(:search) if args.first == search_obj
56
+ concat(hidden_field_tag("#{args.first}[order]", search_obj.order) + "\n")
57
+ super
58
+ else
59
+ super
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,128 @@
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
+ # Returns a new Search object for the given model.
21
+ def search(conditions = {})
22
+ Search.new(self, scope(:find), conditions)
23
+ end
24
+ end
25
+
26
+ # Is an invalid condition is used this error will be raised. Ex:
27
+ #
28
+ # User.search(:unkown => true)
29
+ #
30
+ # Where unknown is not a valid named scope for the User model.
31
+ class UnknownConditionError < StandardError
32
+ def initialize(condition)
33
+ msg = "The #{condition} is not a valid condition. You may only use conditions that map to a named scope"
34
+ super(msg)
35
+ end
36
+ end
37
+
38
+ attr_accessor :klass, :current_scope, :conditions
39
+
40
+ # Creates a new search object for the given class. Ex:
41
+ #
42
+ # Searchlogic::Search.new(User, {}, {:username_like => "bjohnson"})
43
+ def initialize(klass, current_scope, conditions = {})
44
+ self.klass = klass
45
+ self.current_scope = current_scope
46
+ self.conditions = conditions if conditions.is_a?(Hash)
47
+ end
48
+
49
+ # Returns a hash of the current conditions set.
50
+ def conditions
51
+ @conditions ||= {}
52
+ end
53
+
54
+ # Accepts a hash of conditions.
55
+ def conditions=(values)
56
+ values.each do |condition, value|
57
+ value.delete_if { |v| v.blank? } if value.is_a?(Array)
58
+ next if value.blank?
59
+ send("#{condition}=", value)
60
+ end
61
+ end
62
+
63
+ private
64
+ def method_missing(name, *args, &block)
65
+ if name.to_s =~ /(\w+)=$/
66
+ condition = $1.to_sym
67
+ scope_name = normalize_scope_name($1)
68
+ if scope?(scope_name)
69
+ conditions[condition] = type_cast(args.first, cast_type(scope_name))
70
+ else
71
+ raise UnknownConditionError.new(name)
72
+ end
73
+ elsif scope?(normalize_scope_name(name))
74
+ conditions[name]
75
+ else
76
+ scope = conditions.inject(klass.scoped(current_scope)) do |scope, condition|
77
+ scope_name, value = condition
78
+ scope_name = normalize_scope_name(scope_name)
79
+ klass.send(scope_name, value) if !klass.respond_to?(scope_name)
80
+ arity = klass.named_scope_arity(scope_name)
81
+
82
+ if !arity || arity == 0
83
+ if value == true
84
+ scope.send(scope_name)
85
+ else
86
+ scope
87
+ end
88
+ else
89
+ scope.send(scope_name, value)
90
+ end
91
+ end
92
+ scope.send(name, *args, &block)
93
+ end
94
+ end
95
+
96
+ def normalize_scope_name(scope_name)
97
+ klass.column_names.include?(scope_name.to_s) ? "#{scope_name}_equals".to_sym : scope_name.to_sym
98
+ end
99
+
100
+ def scope?(scope_name)
101
+ klass.scopes.key?(scope_name) || klass.condition?(scope_name)
102
+ end
103
+
104
+ def cast_type(name)
105
+ 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_ssope_options
106
+ named_scope_options = klass.named_scope_options(name)
107
+ arity = klass.named_scope_arity(name)
108
+ if !arity || arity == 0
109
+ :boolean
110
+ else
111
+ named_scope_options.respond_to?(:searchlogic_arg_type) ? named_scope_options.searchlogic_arg_type : :string
112
+ end
113
+ end
114
+
115
+ def type_cast(value, type)
116
+ case value
117
+ when Array
118
+ value.collect { |v| type_cast(v, type) }
119
+ else
120
+ # Let's leverage ActiveRecord's type casting, so that casting is consistent
121
+ # with the other models.
122
+ column_for_type_cast = ActiveRecord::ConnectionAdapters::Column.new("", nil)
123
+ column_for_type_cast.instance_variable_set(:@type, type)
124
+ column_for_type_cast.type_cast(value)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,18 @@
1
+ require "searchlogic/core_ext/proc"
2
+ require "searchlogic/core_ext/object"
3
+ require "searchlogic/named_scopes/conditions"
4
+ require "searchlogic/named_scopes/ordering"
5
+ require "searchlogic/named_scopes/associations"
6
+ require "searchlogic/search"
7
+
8
+ Proc.send(:include, Searchlogic::CoreExt::Proc)
9
+ Object.send(:include, Searchlogic::CoreExt::Object)
10
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::Conditions)
11
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering)
12
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::Associations)
13
+ ActiveRecord::Base.extend(Searchlogic::Search::Implementation)
14
+
15
+ if defined?(ActionController)
16
+ require "searchlogic/rails_helpers"
17
+ ActionController::Base.helper(Searchlogic::RailsHelpers)
18
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "searchlogic"
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe "Object" do
4
+ it "should accept and pass the argument to the searchlogic_arg_type" do
5
+ searchlogic_lambda(:integer) {}.searchlogic_arg_type.should == :integer
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe "Proc" do
4
+ it "should have a searchlogic_arg_type accessor" do
5
+ p = Proc.new {}
6
+ p.searchlogic_arg_type = :integer
7
+ p.searchlogic_arg_type.should == :integer
8
+ end
9
+ end
@@ -0,0 +1,124 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe "Associations" do
4
+ before(:each) do
5
+ @users_join_sql = ["LEFT OUTER JOIN \"users\" ON users.company_id = companies.id"]
6
+ @orders_join_sql = ["LEFT OUTER JOIN \"users\" ON users.company_id = companies.id", "LEFT OUTER JOIN \"orders\" ON orders.user_id = users.id"]
7
+ end
8
+
9
+ it "should create a named scope" do
10
+ Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => @users_join_sql)
11
+ end
12
+
13
+ it "should create a deep named scope" do
14
+ Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => @orders_join_sql)
15
+ end
16
+
17
+ it "should not allowed named scopes on non existent association columns" do
18
+ lambda { User.users_whatever_like("bjohnson") }.should raise_error(NoMethodError)
19
+ end
20
+
21
+ it "should not allowed named scopes on non existent deep association columns" do
22
+ lambda { User.users_orders_whatever_like("bjohnson") }.should raise_error(NoMethodError)
23
+ end
24
+
25
+ it "should allow named scopes to be called multiple times and reflect the value passed" do
26
+ Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => @users_join_sql)
27
+ Company.users_username_like("thunt").proxy_options.should == User.username_like("thunt").proxy_options.merge(:joins => @users_join_sql)
28
+ end
29
+
30
+ it "should allow deep named scopes to be called multiple times and reflect the value passed" do
31
+ Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => @orders_join_sql)
32
+ Company.users_orders_total_greater_than(20).proxy_options.should == Order.total_greater_than(20).proxy_options.merge(:joins => @orders_join_sql)
33
+ end
34
+
35
+ it "should have an arity of 1 if the underlying scope has an arity of 1" do
36
+ Company.users_orders_total_greater_than(10)
37
+ Company.named_scope_arity("users_orders_total_greater_than").should == Order.named_scope_arity("total_greater_than")
38
+ end
39
+
40
+ it "should have an arity of nil if the underlying scope has an arity of nil" do
41
+ Company.users_orders_total_null
42
+ Company.named_scope_arity("users_orders_total_null").should == Order.named_scope_arity("total_null")
43
+ end
44
+
45
+ it "should have an arity of -1 if the underlying scope has an arity of -1" do
46
+ Company.users_id_equals_any
47
+ Company.named_scope_arity("users_id_equals_any").should == User.named_scope_arity("id_equals_any")
48
+ end
49
+
50
+ it "should allow aliases" do
51
+ Company.users_username_contains("bjohnson").proxy_options.should == User.username_contains("bjohnson").proxy_options.merge(:joins => @users_join_sql)
52
+ end
53
+
54
+ it "should allow deep aliases" do
55
+ Company.users_orders_total_gt(10).proxy_options.should == Order.total_gt(10).proxy_options.merge(:joins => @orders_join_sql)
56
+ end
57
+
58
+ it "should allow ascending" do
59
+ Company.ascend_by_users_username.proxy_options.should == User.ascend_by_username.proxy_options.merge(:joins => @users_join_sql)
60
+ end
61
+
62
+ it "should allow descending" do
63
+ Company.descend_by_users_username.proxy_options.should == User.descend_by_username.proxy_options.merge(:joins => @users_join_sql)
64
+ end
65
+
66
+ it "should allow deep ascending" do
67
+ Company.ascend_by_users_orders_total.proxy_options.should == Order.ascend_by_total.proxy_options.merge(:joins => @orders_join_sql)
68
+ end
69
+
70
+ it "should allow deep descending" do
71
+ Company.descend_by_users_orders_total.proxy_options.should == Order.descend_by_total.proxy_options.merge(:joins => @orders_join_sql)
72
+ end
73
+
74
+ it "should include optional associations" do
75
+ Company.create
76
+ company = Company.create
77
+ user = company.users.create
78
+ order = user.orders.create(:total => 20, :taxes => 3)
79
+ Company.ascend_by_users_orders_total.all.should == Company.all
80
+ end
81
+
82
+ it "should not create the same join twice" do
83
+ company = Company.create
84
+ user = company.users.create
85
+ order = user.orders.create(:total => 20, :taxes => 3)
86
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all.should == Company.all
87
+ end
88
+
89
+ it "should not create the same join twice when traveling through the duplicate join" do
90
+ Company.users_username_like("bjohnson").users_orders_total_gt(100).all.should == Company.all
91
+ end
92
+
93
+ it "should not create the same join twice when traveling through the duplicate join 2" do
94
+ Company.users_orders_total_gt(100).users_orders_line_items_price_gt(20).all.should == Company.all
95
+ end
96
+
97
+ it "should allow the use of :include when a join was created" do
98
+ company = Company.create
99
+ user = company.users.create
100
+ order = user.orders.create(:total => 20, :taxes => 3)
101
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
102
+ end
103
+
104
+ it "should allow the use of deep :include when a join was created" do
105
+ company = Company.create
106
+ user = company.users.create
107
+ order = user.orders.create(:total => 20, :taxes => 3)
108
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
109
+ end
110
+
111
+ it "should allow the use of :include when traveling through the duplicate join" do
112
+ company = Company.create
113
+ user = company.users.create(:username => "bjohnson")
114
+ order = user.orders.create(:total => 20, :taxes => 3)
115
+ Company.users_username_like("bjohnson").users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
116
+ end
117
+
118
+ it "should allow the use of deep :include when traveling through the duplicate join" do
119
+ company = Company.create
120
+ user = company.users.create(:username => "bjohnson")
121
+ order = user.orders.create(:total => 20, :taxes => 3)
122
+ Company.ascend_by_users_orders_total.users_orders_taxes_lt(50).all(:include => {:users => :orders}).should == Company.all
123
+ end
124
+ end