rd_searchlogic 3.0.0.rc
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 +7 -0
- data/LICENSE +20 -0
- data/README.rdoc +308 -0
- data/Rakefile +42 -0
- data/VERSION.yml +5 -0
- data/init.rb +1 -0
- data/lib/searchlogic/active_record/association_proxy.rb +19 -0
- data/lib/searchlogic/active_record/consistency.rb +49 -0
- data/lib/searchlogic/active_record/named_scope_tools.rb +102 -0
- data/lib/searchlogic/core_ext/object.rb +43 -0
- data/lib/searchlogic/core_ext/proc.rb +17 -0
- data/lib/searchlogic/named_scopes/alias_scope.rb +67 -0
- data/lib/searchlogic/named_scopes/association_conditions.rb +163 -0
- data/lib/searchlogic/named_scopes/association_ordering.rb +44 -0
- data/lib/searchlogic/named_scopes/conditions.rb +232 -0
- data/lib/searchlogic/named_scopes/or_conditions.rb +141 -0
- data/lib/searchlogic/named_scopes/ordering.rb +74 -0
- data/lib/searchlogic/rails_helpers.rb +79 -0
- data/lib/searchlogic/search.rb +259 -0
- data/lib/searchlogic.rb +89 -0
- data/rails/init.rb +1 -0
- data/spec/searchlogic/active_record/association_proxy_spec.rb +23 -0
- data/spec/searchlogic/active_record/consistency_spec.rb +28 -0
- data/spec/searchlogic/core_ext/object_spec.rb +9 -0
- data/spec/searchlogic/core_ext/proc_spec.rb +8 -0
- data/spec/searchlogic/named_scopes/alias_scope_spec.rb +23 -0
- data/spec/searchlogic/named_scopes/association_conditions_spec.rb +221 -0
- data/spec/searchlogic/named_scopes/association_ordering_spec.rb +29 -0
- data/spec/searchlogic/named_scopes/conditions_spec.rb +321 -0
- data/spec/searchlogic/named_scopes/or_conditions_spec.rb +66 -0
- data/spec/searchlogic/named_scopes/ordering_spec.rb +34 -0
- data/spec/searchlogic/search_spec.rb +459 -0
- data/spec/spec_helper.rb +146 -0
- metadata +123 -0
@@ -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
|
+
read_inheritable_attribute(:alias_scopes) || write_inheritable_attribute(: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,163 @@
|
|
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
|
+
def _resolve_deep_association_conditions(condition_name, args)
|
10
|
+
if local_condition?(condition_name)
|
11
|
+
{:joins=>nil, :klass=>self, :condition=>condition_name}
|
12
|
+
elsif details = association_condition_details(condition_name)
|
13
|
+
result = details[:association].klass._resolve_deep_association_conditions(details[:condition], args)
|
14
|
+
return nil unless result #condition method did not resolve
|
15
|
+
this_association = details[:association].name
|
16
|
+
join_condition = result[:joins].nil? ? this_association : {this_association=>result[:joins]}
|
17
|
+
result.merge(:joins=>join_condition)
|
18
|
+
else #this condition method did not resolve
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
private
|
26
|
+
def association_condition?(name)
|
27
|
+
!association_condition_details(name).nil? unless name.to_s.downcase.match("_or_")
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(name, *args, &block)
|
31
|
+
if !local_condition?(name) && details = association_condition_details(name)
|
32
|
+
create_scope_for_association(details[:association], details[:condition], args, details[:poly_class])
|
33
|
+
send(name, *args)
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_scope_for_association(association, condition_name, args, poly_class = nil)
|
40
|
+
result = (poly_class || association.klass)._resolve_deep_association_conditions(condition_name, args)
|
41
|
+
return unless result
|
42
|
+
|
43
|
+
joins_result = result[:joins].nil? ? association.name : {association.name => result[:joins]}
|
44
|
+
|
45
|
+
lambda_containing_relational_algebra = eval <<-"end_eval"
|
46
|
+
lambda { |*args|
|
47
|
+
#{result[:klass].name}.#{result[:condition]}(*args).joins(#{joins_result.inspect})
|
48
|
+
}
|
49
|
+
end_eval
|
50
|
+
|
51
|
+
name = [association.name, poly_class && "#{poly_class.name.underscore}_type", condition_name].compact.join("_")
|
52
|
+
scope(name, lambda_containing_relational_algebra)
|
53
|
+
end
|
54
|
+
|
55
|
+
def association_condition_details(name, last_condition = nil)
|
56
|
+
non_poly_assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
|
57
|
+
poly_assocs = reflect_on_all_associations.reject { |assoc| !assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
|
58
|
+
return nil if non_poly_assocs.empty? && poly_assocs.empty?
|
59
|
+
|
60
|
+
name_with_condition = [name, last_condition].compact.join('_')
|
61
|
+
|
62
|
+
association_name = nil
|
63
|
+
poly_type = nil
|
64
|
+
condition = nil
|
65
|
+
|
66
|
+
if name_with_condition.to_s =~ /^(#{non_poly_assocs.collect(&:name).join("|")})_(\w+)$/
|
67
|
+
association_name = $1
|
68
|
+
condition = $2
|
69
|
+
elsif name_with_condition.to_s =~ /^(#{poly_assocs.collect(&:name).join("|")})_(\w+?)_type_(\w+)$/
|
70
|
+
association_name = $1
|
71
|
+
poly_type = $2
|
72
|
+
condition = $3
|
73
|
+
end
|
74
|
+
|
75
|
+
if association_name && condition
|
76
|
+
association = reflect_on_association(association_name.to_sym)
|
77
|
+
klass = poly_type ? poly_type.camelcase.constantize : association.klass
|
78
|
+
if klass.condition?(condition)
|
79
|
+
{:association => association, :poly_class => poly_type && klass, :condition => condition}
|
80
|
+
else
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
# def association_condition_options(association, association_condition, args, poly_class = nil)
|
88
|
+
# klass = poly_class ? poly_class : association.klass
|
89
|
+
# relation = klass.send(association_condition, *args)
|
90
|
+
# scope_options = nil #klass.named_scope_options(association_condition)
|
91
|
+
# arity = -1 #klass.named_scope_arity(association_condition)
|
92
|
+
#
|
93
|
+
# if !arity || arity == 0
|
94
|
+
# # The underlying condition doesn't require any parameters, so let's just create a simple
|
95
|
+
# # named scope that is based on a hash.
|
96
|
+
# options = {}
|
97
|
+
# in_searchlogic_delegation { options = relation.scope(:find) }
|
98
|
+
# prepare_named_scope_options(options, association, poly_class)
|
99
|
+
# options
|
100
|
+
# else
|
101
|
+
# proc_args = arity_args(arity)
|
102
|
+
# arg_type = :string #(scope_options.respond_to?(:searchlogic_options) && scope_options.searchlogic_options[:type]) || :string
|
103
|
+
#
|
104
|
+
# eval <<-"end_eval"
|
105
|
+
# searchlogic_lambda(:#{arg_type}) { |#{proc_args.join(",")}|
|
106
|
+
# options = {}
|
107
|
+
#
|
108
|
+
# in_searchlogic_delegation do
|
109
|
+
# relation = klass.send(association_condition, #{proc_args.join(",")})
|
110
|
+
# options = {:conditions=>"users.username LIKE '%joe%'"} #relation.scope(:find) if relation
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# prepare_named_scope_options(options, association, poly_class)
|
114
|
+
# options
|
115
|
+
# }
|
116
|
+
# end_eval
|
117
|
+
# end
|
118
|
+
# end
|
119
|
+
|
120
|
+
# Used to match the new scopes parameters to the underlying scope. This way we can disguise the
|
121
|
+
# new scope as best as possible instead of taking the easy way out and using *args.
|
122
|
+
# def arity_args(arity)
|
123
|
+
# args = []
|
124
|
+
# if arity > 0
|
125
|
+
# arity.times { |i| args << "arg#{i}" }
|
126
|
+
# else
|
127
|
+
# positive_arity = arity * -1
|
128
|
+
# positive_arity.times do |i|
|
129
|
+
# if i == (positive_arity - 1)
|
130
|
+
# args << "*arg#{i}"
|
131
|
+
# else
|
132
|
+
# args << "arg#{i}"
|
133
|
+
# end
|
134
|
+
# end
|
135
|
+
# end
|
136
|
+
# args
|
137
|
+
# end
|
138
|
+
|
139
|
+
# #ADDED: this was removed from AR::Base ver2.x, redefined for use in prepare_named_scope_options
|
140
|
+
# def array_of_strings?(o)
|
141
|
+
# o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# def prepare_named_scope_options(options, association, poly_class = nil)
|
145
|
+
# options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
|
146
|
+
#
|
147
|
+
# klass = poly_class || association.klass
|
148
|
+
# # sanitize the conditions locally so we get the right table name, otherwise the conditions will be evaluated on the original model
|
149
|
+
# options[:conditions] = klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)
|
150
|
+
#
|
151
|
+
# poly_join = poly_class && inner_polymorphic_join(poly_class.name.underscore, :as => association.name)
|
152
|
+
#
|
153
|
+
# if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
|
154
|
+
# options[:joins] = [poly_class ? poly_join : inner_joins(association.name), options[:joins]].flatten
|
155
|
+
# elsif poly_class
|
156
|
+
# options[:joins] = options[:joins].blank? ? poly_join : ([poly_join] + klass.inner_joins(options[:joins]))
|
157
|
+
# else
|
158
|
+
# options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
|
159
|
+
# end
|
160
|
+
# end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,44 @@
|
|
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
|
33
|
+
association_names = associations.collect { |assoc| assoc.name }
|
34
|
+
if name.to_s =~ /^(ascend|descend)_by_(#{association_names.join("|")})_(\w+)$/
|
35
|
+
{:order_as => $1, :association => associations.find { |a| a.name == $2.to_sym }, :condition => $3}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_association_ordering_condition(association, order_as, condition, args)
|
40
|
+
scope("#{order_as}_by_#{association.name}_#{condition}", association_condition_options(association, "#{order_as}_by_#{condition}", args))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,232 @@
|
|
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 => [:does_not_include],
|
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
|
+
CONDITIONS = {}
|
39
|
+
|
40
|
+
# Add any / all variations to every comparison and wildcard condition
|
41
|
+
COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases|
|
42
|
+
CONDITIONS[condition] = aliases
|
43
|
+
CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym }
|
44
|
+
CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym }
|
45
|
+
end
|
46
|
+
|
47
|
+
CONDITIONS[:equals_any] = CONDITIONS[:equals_any] + [:in]
|
48
|
+
CONDITIONS[:does_not_equal_all] = CONDITIONS[:does_not_equal_all] + [:not_in]
|
49
|
+
|
50
|
+
BOOLEAN_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases }
|
51
|
+
|
52
|
+
PRIMARY_CONDITIONS = CONDITIONS.keys
|
53
|
+
ALIAS_CONDITIONS = CONDITIONS.values.flatten
|
54
|
+
|
55
|
+
def options
|
56
|
+
@options
|
57
|
+
end
|
58
|
+
|
59
|
+
# Is the name of the method a valid condition that can be dynamically created?
|
60
|
+
def condition?(name)
|
61
|
+
local_condition?(name)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def local_condition?(name)
|
66
|
+
return false if name.blank?
|
67
|
+
scope_names = scopes.keys.reject { |k| k == :scoped }
|
68
|
+
scope_names.include?(name.to_sym) || !condition_details(name).nil? || boolean_condition?(name)
|
69
|
+
end
|
70
|
+
|
71
|
+
def boolean_condition?(name)
|
72
|
+
column = columns_hash[name.to_s] || columns_hash[name.to_s.gsub(/^not_/, "")]
|
73
|
+
column && column.type == :boolean
|
74
|
+
end
|
75
|
+
|
76
|
+
def method_missing(name, *args, &block)
|
77
|
+
if details = condition_details(name)
|
78
|
+
create_scope(details[:column], details[:condition], args)
|
79
|
+
send(name, *args)
|
80
|
+
elsif boolean_condition?(name)
|
81
|
+
column = name.to_s.gsub(/^not_/, "")
|
82
|
+
scope name, where(column.to_sym => (name.to_s =~ /^not_/).nil?) #:conditions => {column => (name.to_s =~ /^not_/).nil?}
|
83
|
+
send(name)
|
84
|
+
else
|
85
|
+
super
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def condition_details(method_name)
|
91
|
+
#TODO: cache these
|
92
|
+
column_name_matcher = column_names.join("|")
|
93
|
+
conditions_matcher = (PRIMARY_CONDITIONS + ALIAS_CONDITIONS).join("|")
|
94
|
+
|
95
|
+
if method_name.to_s =~ /^(#{column_name_matcher})_(#{conditions_matcher})$/
|
96
|
+
{:column => $1, :condition => $2}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def create_scope(column, condition, args)
|
101
|
+
if PRIMARY_CONDITIONS.include?(condition.to_sym)
|
102
|
+
create_primary_scope(column, condition)
|
103
|
+
elsif ALIAS_CONDITIONS.include?(condition.to_sym)
|
104
|
+
create_aliased_scope(column, condition, args)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def create_primary_scope(column, condition)
|
109
|
+
column_type = columns_hash[column.to_s].type
|
110
|
+
skip_conversion = skip_time_zone_conversion_for_attributes.include?(columns_hash[column.to_s].name.to_sym)
|
111
|
+
match_keyword = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE"
|
112
|
+
|
113
|
+
scope_options = case condition.to_s
|
114
|
+
when /^equals/
|
115
|
+
#scope_options(condition, column_type, lambda { |a| attribute_condition("#{table_name}.#{column}", a) }, :skip_conversion => skip_conversion)
|
116
|
+
scope_options(condition, column_type, "#{table_name}.#{column} = ?", :skip_conversion => skip_conversion)
|
117
|
+
when /^does_not_equal/
|
118
|
+
scope_options(condition, column_type, "#{table_name}.#{column} != ?", :skip_conversion => skip_conversion)
|
119
|
+
when /^less_than_or_equal_to/
|
120
|
+
scope_options(condition, column_type, "#{table_name}.#{column} <= ?", :skip_conversion => skip_conversion)
|
121
|
+
when /^less_than/
|
122
|
+
scope_options(condition, column_type, "#{table_name}.#{column} < ?", :skip_conversion => skip_conversion)
|
123
|
+
when /^greater_than_or_equal_to/
|
124
|
+
scope_options(condition, column_type, "#{table_name}.#{column} >= ?", :skip_conversion => skip_conversion)
|
125
|
+
when /^greater_than/
|
126
|
+
scope_options(condition, column_type, "#{table_name}.#{column} > ?", :skip_conversion => skip_conversion)
|
127
|
+
when /^like/
|
128
|
+
scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :like)
|
129
|
+
when /^not_like/
|
130
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :like)
|
131
|
+
when /^begins_with/
|
132
|
+
scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :begins_with)
|
133
|
+
when /^not_begin_with/
|
134
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :begins_with)
|
135
|
+
when /^ends_with/
|
136
|
+
scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :ends_with)
|
137
|
+
when /^not_end_with/
|
138
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :ends_with)
|
139
|
+
when "null"
|
140
|
+
lambda { where("#{table_name}.#{column} IS NULL")}
|
141
|
+
when "not_null"
|
142
|
+
lambda { where("#{table_name}.#{column} IS NOT NULL")}
|
143
|
+
when "empty"
|
144
|
+
lambda { where("#{table_name}.#{column} = ''")}
|
145
|
+
when "blank"
|
146
|
+
lambda { where("#{table_name}.#{column} = '' OR #{table_name}.#{column} IS NULL")}
|
147
|
+
when "not_blank"
|
148
|
+
lambda { where("#{table_name}.#{column} != '' AND #{table_name}.#{column} IS NOT NULL")}
|
149
|
+
end
|
150
|
+
|
151
|
+
scope("#{column}_#{condition}".to_sym, scope_options)
|
152
|
+
end
|
153
|
+
|
154
|
+
# This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions.
|
155
|
+
# Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as
|
156
|
+
# fast as possible, which is why I didn't do the case statement inside of the lambda.
|
157
|
+
def scope_options(condition, column_type, sql, options = {})
|
158
|
+
case condition.to_s
|
159
|
+
when /_(any|all)$/
|
160
|
+
searchlogic_lambda(column_type, :skip_conversion => options[:skip_conversion]) { |*values|
|
161
|
+
unless values.empty?
|
162
|
+
values.flatten!
|
163
|
+
values.collect! { |value| value_with_modifier(value, options[:value_modifier]) }
|
164
|
+
|
165
|
+
join = $1 == "any" ? " OR " : " AND "
|
166
|
+
|
167
|
+
scope_sql = values.collect { |value| sql.is_a?(Proc) ? sql.call(value) : sql }.join(join)
|
168
|
+
|
169
|
+
lambda { where [scope_sql, *expand_range_bind_variables(values)]}
|
170
|
+
else
|
171
|
+
{}
|
172
|
+
end
|
173
|
+
}
|
174
|
+
else
|
175
|
+
searchlogic_lambda(column_type, :skip_conversion => options[:skip_conversion]) { |*values|
|
176
|
+
values.collect! { |value| value_with_modifier(value, options[:value_modifier]) }
|
177
|
+
|
178
|
+
scope_sql = sql.is_a?(Proc) ? sql.call(*values) : sql
|
179
|
+
|
180
|
+
{:conditions => [scope_sql, *expand_range_bind_variables(values)]}
|
181
|
+
}
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def value_with_modifier(value, modifier)
|
186
|
+
case modifier
|
187
|
+
when :like
|
188
|
+
"%#{value}%"
|
189
|
+
when :begins_with
|
190
|
+
"#{value}%"
|
191
|
+
when :ends_with
|
192
|
+
"%#{value}"
|
193
|
+
else
|
194
|
+
value
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def create_aliased_scope(column, condition, args)
|
199
|
+
primary_condition = primary_condition(condition)
|
200
|
+
alias_name = "#{column}_#{condition}"
|
201
|
+
primary_name = "#{column}_#{primary_condition}"
|
202
|
+
send(primary_name, *args) # go back to method_missing and make sure we create the method
|
203
|
+
(class << self; self; end).class_eval { alias_method alias_name, primary_name }
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns the primary condition for the given alias. Ex:
|
207
|
+
#
|
208
|
+
# primary_condition(:gt) => :greater_than
|
209
|
+
def primary_condition(alias_condition)
|
210
|
+
CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
|
211
|
+
end
|
212
|
+
|
213
|
+
# Returns the primary name for any condition on a column. You can pass it
|
214
|
+
# a primary condition, alias condition, etc, and it will return the proper
|
215
|
+
# primary condition name. This helps simply logic throughout Searchlogic. Ex:
|
216
|
+
#
|
217
|
+
# condition_scope_name(:id_gt) => :id_greater_than
|
218
|
+
# condition_scope_name(:id_greater_than) => :id_greater_than
|
219
|
+
def condition_scope_name(name)
|
220
|
+
if details = condition_details(name)
|
221
|
+
if PRIMARY_CONDITIONS.include?(name.to_sym)
|
222
|
+
name
|
223
|
+
else
|
224
|
+
"#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
|
225
|
+
end
|
226
|
+
else
|
227
|
+
nil
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module NamedScopes
|
3
|
+
# Handles dynamically creating named scopes for 'OR' conditions. Please see the README for a more
|
4
|
+
# detailed explanation.
|
5
|
+
module OrConditions
|
6
|
+
class NoConditionSpecifiedError < StandardError; end
|
7
|
+
class UnknownConditionError < StandardError; end
|
8
|
+
|
9
|
+
def condition?(name) # :nodoc:
|
10
|
+
super || or_condition?(name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def named_scope_options(name) # :nodoc:
|
14
|
+
super || super(or_conditions(name).join("_or_"))
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def or_condition?(name)
|
19
|
+
!or_conditions(name).nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing(name, *args, &block)
|
23
|
+
if conditions = or_conditions(name)
|
24
|
+
create_or_condition(conditions, args)
|
25
|
+
(class << self; self; end).class_eval { alias_method name, conditions.join("_or_") } if !respond_to?(name)
|
26
|
+
send(name, *args)
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def or_conditions(name)
|
33
|
+
# First determine if we should even work on the name, we want to be as quick as possible
|
34
|
+
# with this.
|
35
|
+
if (parts = split_or_condition(name)).size > 1
|
36
|
+
conditions = interpolate_or_conditions(parts)
|
37
|
+
if conditions.any?
|
38
|
+
conditions
|
39
|
+
else
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def split_or_condition(name)
|
46
|
+
parts = name.to_s.split("_or_")
|
47
|
+
new_parts = []
|
48
|
+
parts.each do |part|
|
49
|
+
if part =~ /^equal_to(_any|_all)?$/
|
50
|
+
new_parts << new_parts.pop + "_or_equal_to"
|
51
|
+
else
|
52
|
+
new_parts << part
|
53
|
+
end
|
54
|
+
end
|
55
|
+
new_parts
|
56
|
+
end
|
57
|
+
|
58
|
+
# The purpose of this method is to convert the method name parts into actual condition names.
|
59
|
+
#
|
60
|
+
# Example:
|
61
|
+
#
|
62
|
+
# ["first_name", "last_name_like"]
|
63
|
+
# => ["first_name_like", "last_name_like"]
|
64
|
+
#
|
65
|
+
# ["id_gt", "first_name_begins_with", "last_name", "middle_name_like"]
|
66
|
+
# => ["id_gt", "first_name_begins_with", "last_name_like", "middle_name_like"]
|
67
|
+
#
|
68
|
+
# Basically if a column is specified without a condition the next condition in the list
|
69
|
+
# is what will be used. Once we are able to get a consistent list of conditions we can easily
|
70
|
+
# create a scope for it.
|
71
|
+
def interpolate_or_conditions(parts)
|
72
|
+
conditions = []
|
73
|
+
last_condition = nil
|
74
|
+
|
75
|
+
parts.reverse.each do |part|
|
76
|
+
if details = condition_details(part)
|
77
|
+
# We are a searchlogic defined scope
|
78
|
+
conditions << "#{details[:column]}_#{details[:condition]}"
|
79
|
+
last_condition = details[:condition]
|
80
|
+
elsif association_details = association_condition_details(part, last_condition)
|
81
|
+
path = full_association_path(part, last_condition, association_details[:association])
|
82
|
+
conditions << "#{path[:path].join("_").to_sym}_#{path[:column]}_#{path[:condition]}"
|
83
|
+
last_condition = path[:condition] || nil
|
84
|
+
elsif local_condition?(part)
|
85
|
+
# We are a custom scope
|
86
|
+
conditions << part
|
87
|
+
elsif column_names.include?(part)
|
88
|
+
# we are a column, use the last condition
|
89
|
+
if last_condition.nil?
|
90
|
+
raise NoConditionSpecifiedError.new("The '#{part}' column doesn't know which condition to use, if you use an exact column " +
|
91
|
+
"name you need to specify a condition sometime after (ex: id_or_created_at_lt), where id would use the 'lt' condition.")
|
92
|
+
end
|
93
|
+
|
94
|
+
conditions << "#{part}_#{last_condition}"
|
95
|
+
else
|
96
|
+
raise UnknownConditionError.new("The condition '#{part}' is not a valid condition, we could not find any scopes that match this.")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
conditions.reverse
|
101
|
+
end
|
102
|
+
|
103
|
+
def full_association_path(part, last_condition, given_assoc)
|
104
|
+
path = [given_assoc.name]
|
105
|
+
part.sub!(/^#{given_assoc.name}_/, "")
|
106
|
+
klass = self
|
107
|
+
while klass = klass.send(:reflect_on_association, given_assoc.name)
|
108
|
+
klass = klass.klass
|
109
|
+
if details = klass.send(:association_condition_details, part, last_condition)
|
110
|
+
path << details[:association]
|
111
|
+
part = details[:condition]
|
112
|
+
given_assoc = details[:association]
|
113
|
+
elsif details = klass.send(:condition_details, part)
|
114
|
+
return { :path => path, :column => details[:column], :condition => details[:condition] }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
{ :path => path, :column => part, :condition => last_condition }
|
118
|
+
end
|
119
|
+
|
120
|
+
def create_or_condition(scopes, args)
|
121
|
+
scopes_options = scopes.collect { |scope, *args| send(scope, *args).proxy_options }
|
122
|
+
# We're using first scope to determine column's type
|
123
|
+
scope = named_scope_options(scopes.first)
|
124
|
+
column_type = scope.respond_to?(:searchlogic_options) ? scope.searchlogic_options[:type] : :string
|
125
|
+
scope scopes.join("_or_"), searchlogic_lambda(column_type) { |*args|
|
126
|
+
merge_scopes_with_or(scopes.collect { |scope| clone.send(scope, *args) })
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def merge_scopes_with_or(scopes)
|
131
|
+
scopes_options = scopes.collect { |scope| scope.scope(:find) }
|
132
|
+
conditions = scopes_options.reject { |o| o[:conditions].nil? }.collect { |o| sanitize_sql(o[:conditions]) }
|
133
|
+
scope = scopes_options.inject(scoped({})) { |current_scope, options| current_scope.scoped(options) }
|
134
|
+
options = {}
|
135
|
+
in_searchlogic_delegation { options = scope.scope(:find) }
|
136
|
+
options.delete(:readonly) unless scopes.any? { |scope| scope.proxy_options.key?(:readonly) }
|
137
|
+
options.merge(:conditions => "(" + conditions.join(") OR (") + ")")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|