searchlogic-donotuse 2.3.9

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,41 @@
1
+ module Searchlogic
2
+ module CoreExt
3
+ # Contains extensions for the Object class that Searchlogic uses.
4
+ module Object
5
+ # Searchlogic needs to know the expected type of the condition value so that it can properly cast
6
+ # the value in the Searchlogic::Search object. For example:
7
+ #
8
+ # search = User.search(:id_gt => "1")
9
+ #
10
+ # You would expect this:
11
+ #
12
+ # search.id_gt => 1
13
+ #
14
+ # Not this:
15
+ #
16
+ # search.id_gt => "1"
17
+ #
18
+ # Parameter values from forms are ALWAYS strings, so we have to cast them. Just like ActiveRecord
19
+ # does when you instantiate a new User object.
20
+ #
21
+ # The problem is that ruby has no variable types, so Searchlogic needs to know what type you are expecting
22
+ # for your named scope. So instead of this:
23
+ #
24
+ # named_scope :id_gt, lambda { |value| {:conditions => ["id > ?", value]} }
25
+ #
26
+ # You need to do this:
27
+ #
28
+ # named_scope :id_gt, searchlogic_lambda(:integer) { |value| {:conditions => ["id > ?", value]} }
29
+ #
30
+ # If you are wanting a string, you don't have to do anything, because Searchlogic assumes you want a string.
31
+ # If you want something else, you need to specify it as I did in the above example. Comments are appreciated
32
+ # on this, if you know of a better solution please let me know. But this is the best I could come up with,
33
+ # without being intrusive and altering default behavior.
34
+ def searchlogic_lambda(type = :string, &block)
35
+ proc = lambda(&block)
36
+ proc.searchlogic_arg_type = type
37
+ proc
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ module Searchlogic
2
+ module CoreExt
3
+ module Proc # :nodoc:
4
+ def self.included(klass)
5
+ klass.class_eval do
6
+ attr_accessor :searchlogic_arg_type
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,67 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Adds the ability to create alias scopes that allow you to alias a named
4
+ # scope or create a named scope procedure. See the alias_scope method for a more
5
+ # detailed explanation.
6
+ module AliasScope
7
+ # In some instances you might create a class method that essentially aliases a named scope
8
+ # or represents a named scope procedure. Ex:
9
+ #
10
+ # class User
11
+ # def teenager
12
+ # age_gte(13).age_lte(19)
13
+ # end
14
+ # end
15
+ #
16
+ # This is obviously a very basic example, but notice how we are utilizing already existing named
17
+ # scopes so that we do not have to repeat ourself. This method makes a lot more sense when you are
18
+ # dealing with complicated named scope.
19
+ #
20
+ # There is a problem though. What if you want to use this in your controller's via the 'search' method:
21
+ #
22
+ # User.search(:teenager => true)
23
+ #
24
+ # You would expect that to work, but how does Searchlogic::Search tell the difference between your
25
+ # 'teenager' method and the 'destroy_all' method. It can't, there is no way to tell unless we actually
26
+ # call the method, which we obviously can not do.
27
+ #
28
+ # The being said, we need a way to tell searchlogic that this is method is safe. Here's how you do that:
29
+ #
30
+ # User.alias_scope :teenager, lambda { age_gte(13).age_lte(19) }
31
+ #
32
+ # This feels better, it feels like our other scopes, and it provides a way to tell Searchlogic that this
33
+ # is a safe method.
34
+ def alias_scope(name, options = nil)
35
+ alias_scopes[name.to_sym] = options
36
+ (class << self; self end).instance_eval do
37
+ define_method name do |*args|
38
+ case options
39
+ when Symbol
40
+ send(options)
41
+ else
42
+ options.call(*args)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ alias_method :scope_procedure, :alias_scope
48
+
49
+ def alias_scopes # :nodoc:
50
+ @alias_scopes ||= {}
51
+ end
52
+
53
+ def alias_scope?(name) # :nodoc:
54
+ return false if name.blank?
55
+ alias_scopes.key?(name.to_sym)
56
+ end
57
+
58
+ def condition?(name) # :nodoc:
59
+ super || alias_scope?(name)
60
+ end
61
+
62
+ def named_scope_options(name) # :nodoc:
63
+ super || alias_scopes[name.to_sym]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,102 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for associations. See the README for a detailed explanation.
4
+ module AssociationConditions
5
+ def condition?(name) # :nodoc:
6
+ super || association_condition?(name)
7
+ end
8
+
9
+ private
10
+ def association_condition?(name)
11
+ !association_condition_details(name).nil? unless name.to_s.downcase.match("_or_")
12
+ end
13
+
14
+ def method_missing(name, *args, &block)
15
+ if !local_condition?(name) && details = association_condition_details(name)
16
+ create_association_condition(details[:association], details[:condition], args)
17
+ send(name, *args)
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def association_condition_details(name, last_condition = nil)
24
+ assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
25
+ return nil if assocs.empty?
26
+
27
+ name_with_condition = [name, last_condition].compact.join('_')
28
+ if name_with_condition.to_s =~ /^(#{assocs.collect(&:name).join("|")})_(\w+)$/
29
+ association_name = $1
30
+ condition = $2
31
+ association = reflect_on_association(association_name.to_sym)
32
+ klass = association.klass
33
+ if klass.condition?(condition)
34
+ {:association => $1, :condition => $2}
35
+ else
36
+ nil
37
+ end
38
+ end
39
+ end
40
+
41
+ def create_association_condition(association, condition, args)
42
+ named_scope("#{association}_#{condition}", association_condition_options(association, condition, args))
43
+ end
44
+
45
+ def association_condition_options(association_name, association_condition, args)
46
+ association = reflect_on_association(association_name.to_sym)
47
+ scope = association.klass.send(association_condition, *args)
48
+ scope_options = association.klass.named_scope_options(association_condition)
49
+ arity = association.klass.named_scope_arity(association_condition)
50
+
51
+ if !arity || arity == 0
52
+ # The underlying condition doesn't require any parameters, so let's just create a simple
53
+ # named scope that is based on a hash.
54
+ options = scope.scope(:find)
55
+ prepare_named_scope_options(options, association)
56
+ options
57
+ else
58
+ proc_args = arity_args(arity)
59
+ arg_type = (scope_options.respond_to?(:searchlogic_arg_type) && scope_options.searchlogic_arg_type) || :string
60
+
61
+ eval <<-"end_eval"
62
+ searchlogic_lambda(:#{arg_type}) { |#{proc_args.join(",")}|
63
+ scope = association.klass.send(association_condition, #{proc_args.join(",")})
64
+ options = scope ? scope.scope(:find) : {}
65
+ prepare_named_scope_options(options, association)
66
+ options
67
+ }
68
+ end_eval
69
+ end
70
+ end
71
+
72
+ # Used to match the new scopes parameters to the underlying scope. This way we can disguise the
73
+ # new scope as best as possible instead of taking the easy way out and using *args.
74
+ def arity_args(arity)
75
+ args = []
76
+ if arity > 0
77
+ arity.times { |i| args << "arg#{i}" }
78
+ else
79
+ positive_arity = arity * -1
80
+ positive_arity.times do |i|
81
+ if i == (positive_arity - 1)
82
+ args << "*arg#{i}"
83
+ else
84
+ args << "arg#{i}"
85
+ end
86
+ end
87
+ end
88
+ args
89
+ end
90
+
91
+ def prepare_named_scope_options(options, association)
92
+ options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
93
+
94
+ if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
95
+ options[:joins] = [inner_joins(association.name), options[:joins]].flatten
96
+ else
97
+ options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,43 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating order named scopes for associations:
4
+ #
5
+ # User.has_many :orders
6
+ # Order.has_many :line_items
7
+ # LineItem
8
+ #
9
+ # User.ascend_by_orders_line_items_id
10
+ #
11
+ # See the README for a more detailed explanation.
12
+ module AssociationOrdering
13
+ def condition?(name) # :nodoc:
14
+ super || association_ordering_condition?(name)
15
+ end
16
+
17
+ private
18
+ def association_ordering_condition?(name)
19
+ !association_ordering_condition_details(name).nil?
20
+ end
21
+
22
+ def method_missing(name, *args, &block)
23
+ if details = association_ordering_condition_details(name)
24
+ create_association_ordering_condition(details[:association], details[:order_as], details[:condition], args)
25
+ send(name, *args)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def association_ordering_condition_details(name)
32
+ associations = reflect_on_all_associations.collect { |assoc| assoc.name }
33
+ if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
34
+ {:order_as => $1, :association => $2, :condition => $3}
35
+ end
36
+ end
37
+
38
+ def create_association_ordering_condition(association, order_as, condition, args)
39
+ named_scope("#{order_as}_by_#{association}_#{condition}", association_condition_options(association, "#{order_as}_by_#{condition}", args))
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,229 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for columns. It allows you to do things like:
4
+ #
5
+ # User.first_name_like("ben")
6
+ # User.id_lt(10)
7
+ #
8
+ # Notice the constants in this class, they define which conditions Searchlogic provides.
9
+ #
10
+ # See the README for a more detailed explanation.
11
+ module Conditions
12
+ COMPARISON_CONDITIONS = {
13
+ :equals => [:is, :eq],
14
+ :does_not_equal => [:not_equal_to, :is_not, :not, :ne],
15
+ :less_than => [:lt, :before],
16
+ :less_than_or_equal_to => [:lte],
17
+ :greater_than => [:gt, :after],
18
+ :greater_than_or_equal_to => [:gte],
19
+ }
20
+
21
+ WILDCARD_CONDITIONS = {
22
+ :like => [:contains, :includes],
23
+ :not_like => [],
24
+ :begins_with => [:bw],
25
+ :not_begin_with => [:does_not_begin_with],
26
+ :ends_with => [:ew],
27
+ :not_end_with => [:does_not_end_with]
28
+ }
29
+
30
+ BOOLEAN_CONDITIONS = {
31
+ :null => [:nil],
32
+ :not_null => [:not_nil],
33
+ :empty => [],
34
+ :blank => [],
35
+ :not_blank => [:present]
36
+ }
37
+
38
+ GROUP_CONDITIONS = {
39
+ :in => [],
40
+ :not_in => []
41
+ }
42
+
43
+ CONDITIONS = {}
44
+
45
+ # Add any / all variations to every comparison and wildcard condition
46
+ COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases|
47
+ CONDITIONS[condition] = aliases
48
+ CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym }
49
+ CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym }
50
+ end
51
+
52
+ CONDITIONS[:equals_any] = CONDITIONS[:equals_any] + [:in]
53
+ CONDITIONS[:does_not_equal_any] = CONDITIONS[:equals_any] + [:not_in]
54
+
55
+ BOOLEAN_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases }
56
+
57
+ GROUP_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases }
58
+
59
+ PRIMARY_CONDITIONS = CONDITIONS.keys
60
+ ALIAS_CONDITIONS = CONDITIONS.values.flatten
61
+
62
+ # Is the name of the method a valid condition that can be dynamically created?
63
+ def condition?(name)
64
+ local_condition?(name)
65
+ end
66
+
67
+ private
68
+ def local_condition?(name)
69
+ return false if name.blank?
70
+ scope_names = scopes.keys.reject { |k| k == :scoped }
71
+ scope_names.include?(name.to_sym) || !condition_details(name).nil? || boolean_condition?(name)
72
+ end
73
+
74
+ def boolean_condition?(name)
75
+ column = columns_hash[name.to_s] || columns_hash[name.to_s.gsub(/^not_/, "")]
76
+ column && column.type == :boolean
77
+ end
78
+
79
+ def method_missing(name, *args, &block)
80
+ if details = condition_details(name)
81
+ create_condition(details[:column], details[:condition], args)
82
+ send(name, *args)
83
+ elsif boolean_condition?(name)
84
+ column = name.to_s.gsub(/^not_/, "")
85
+ named_scope name, :conditions => {column => (name.to_s =~ /^not_/).nil?}
86
+ send(name)
87
+ else
88
+ super
89
+ end
90
+ end
91
+
92
+ def condition_details(name, *args)
93
+ if args.size > 0 and !args.first.nil?
94
+ assoc = reflect_on_association(args.first.to_sym)
95
+ klass = assoc.klass
96
+ name.to_s =~ /^(#{klass.column_names.join("|")})_(#{(PRIMARY_CONDITIONS + ALIAS_CONDITIONS).join("|")})$/
97
+ {:column => $1, :condition => $2}
98
+ elsif name.to_s =~ /^(#{column_names.join("|")})_(#{(PRIMARY_CONDITIONS + ALIAS_CONDITIONS).join("|")})$/
99
+ {:column => $1, :condition => $2}
100
+ end
101
+ end
102
+
103
+ def create_condition(column, condition, args)
104
+ if PRIMARY_CONDITIONS.include?(condition.to_sym)
105
+ create_primary_condition(column, condition)
106
+ elsif ALIAS_CONDITIONS.include?(condition.to_sym)
107
+ create_alias_condition(column, condition, args)
108
+ end
109
+ end
110
+
111
+ def create_primary_condition(column, condition)
112
+ column_type = columns_hash[column.to_s].type
113
+ match_keyword = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE"
114
+
115
+ scope_options = case condition.to_s
116
+ when /^equals/
117
+ scope_options(condition, column_type, lambda { |a| attribute_condition("#{table_name}.#{column}", a) })
118
+ when /^does_not_equal/
119
+ scope_options(condition, column_type, "#{table_name}.#{column} != ?")
120
+ when /^less_than_or_equal_to/
121
+ scope_options(condition, column_type, "#{table_name}.#{column} <= ?")
122
+ when /^less_than/
123
+ scope_options(condition, column_type, "#{table_name}.#{column} < ?")
124
+ when /^greater_than_or_equal_to/
125
+ scope_options(condition, column_type, "#{table_name}.#{column} >= ?")
126
+ when /^greater_than/
127
+ scope_options(condition, column_type, "#{table_name}.#{column} > ?")
128
+ when /^like/
129
+ scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :like)
130
+ when /^not_like/
131
+ scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :like)
132
+ when /^begins_with/
133
+ scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :begins_with)
134
+ when /^not_begin_with/
135
+ scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :begins_with)
136
+ when /^ends_with/
137
+ scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :ends_with)
138
+ when /^not_end_with/
139
+ scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :ends_with)
140
+ when "null"
141
+ {:conditions => "#{table_name}.#{column} IS NULL"}
142
+ when "not_null"
143
+ {:conditions => "#{table_name}.#{column} IS NOT NULL"}
144
+ when "empty"
145
+ {:conditions => "#{table_name}.#{column} = ''"}
146
+ when "blank"
147
+ {:conditions => "#{table_name}.#{column} = '' OR #{table_name}.#{column} IS NULL"}
148
+ when "not_blank"
149
+ {:conditions => "#{table_name}.#{column} != '' AND #{table_name}.#{column} IS NOT NULL"}
150
+ end
151
+
152
+ named_scope("#{column}_#{condition}".to_sym, scope_options)
153
+ end
154
+
155
+ # This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions.
156
+ # Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as
157
+ # fast as possible, which is why I didn't do the case statement inside of the lambda.
158
+ def scope_options(condition, column_type, sql, value_modifier = nil)
159
+ case condition.to_s
160
+ when /_(any|all)$/
161
+ searchlogic_lambda(column_type) { |*values|
162
+ return {} if values.empty?
163
+ values.flatten!
164
+ values.collect! { |value| value_with_modifier(value, value_modifier) }
165
+
166
+ join = $1 == "any" ? " OR " : " AND "
167
+ scope_sql = values.collect { |value| sql.is_a?(Proc) ? sql.call(value) : sql }.join(join)
168
+
169
+ {:conditions => [scope_sql, *expand_range_bind_variables(values)]}
170
+ }
171
+ else
172
+ searchlogic_lambda(column_type) { |*values|
173
+ values.collect! { |value| value_with_modifier(value, value_modifier) }
174
+
175
+ scope_sql = sql.is_a?(Proc) ? sql.call(*values) : sql
176
+
177
+ {:conditions => [scope_sql, *expand_range_bind_variables(values)]}
178
+ }
179
+ end
180
+ end
181
+
182
+ def value_with_modifier(value, modifier)
183
+ case modifier
184
+ when :like
185
+ "%#{value}%"
186
+ when :begins_with
187
+ "#{value}%"
188
+ when :ends_with
189
+ "%#{value}"
190
+ else
191
+ value
192
+ end
193
+ end
194
+
195
+ def create_alias_condition(column, condition, args)
196
+ primary_condition = primary_condition(condition)
197
+ alias_name = "#{column}_#{condition}"
198
+ primary_name = "#{column}_#{primary_condition}"
199
+ send(primary_name, *args) # go back to method_missing and make sure we create the method
200
+ (class << self; self; end).class_eval { alias_method alias_name, primary_name }
201
+ end
202
+
203
+ # Returns the primary condition for the given alias. Ex:
204
+ #
205
+ # primary_condition(:gt) => :greater_than
206
+ def primary_condition(alias_condition)
207
+ CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
208
+ end
209
+
210
+ # Returns the primary name for any condition on a column. You can pass it
211
+ # a primary condition, alias condition, etc, and it will return the proper
212
+ # primary condition name. This helps simply logic throughout Searchlogic. Ex:
213
+ #
214
+ # condition_scope_name(:id_gt) => :id_greater_than
215
+ # condition_scope_name(:id_greater_than) => :id_greater_than
216
+ def condition_scope_name(name)
217
+ if details = condition_details(name)
218
+ if PRIMARY_CONDITIONS.include?(name.to_sym)
219
+ name
220
+ else
221
+ "#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
222
+ end
223
+ else
224
+ nil
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end