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.
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +191 -0
- data/Rakefile +48 -0
- data/VERSION.yml +4 -0
- data/init.rb +1 -0
- data/lib/searchlogic/core_ext/object.rb +39 -0
- data/lib/searchlogic/core_ext/proc.rb +11 -0
- data/lib/searchlogic/named_scopes/associations.rb +144 -0
- data/lib/searchlogic/named_scopes/conditions.rb +215 -0
- data/lib/searchlogic/named_scopes/ordering.rb +53 -0
- data/lib/searchlogic/rails_helpers.rb +63 -0
- data/lib/searchlogic/search.rb +128 -0
- data/lib/searchlogic.rb +18 -0
- data/rails/init.rb +1 -0
- data/spec/core_ext/object_spec.rb +7 -0
- data/spec/core_ext/proc_spec.rb +9 -0
- data/spec/named_scopes/associations_spec.rb +124 -0
- data/spec/named_scopes/conditions_spec.rb +253 -0
- data/spec/named_scopes/ordering_spec.rb +23 -0
- data/spec/search_spec.rb +251 -0
- data/spec/spec_helper.rb +77 -0
- metadata +81 -0
@@ -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] = "▲ #{options[:as]}"
|
28
|
+
css_classes << "ascending"
|
29
|
+
else
|
30
|
+
options[:as] = "▼ #{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
|
data/lib/searchlogic.rb
ADDED
@@ -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,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
|