lazy-searchlogic 2.4.10
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 +308 -0
- data/Rakefile +35 -0
- data/VERSION.yml +5 -0
- data/init.rb +1 -0
- data/lib/searchlogic.rb +40 -0
- data/lib/searchlogic/active_record/consistency.rb +49 -0
- data/lib/searchlogic/active_record/named_scope_tools.rb +101 -0
- data/lib/searchlogic/core_ext/object.rb +41 -0
- data/lib/searchlogic/core_ext/proc.rb +11 -0
- data/lib/searchlogic/named_scopes/alias_scope.rb +67 -0
- data/lib/searchlogic/named_scopes/association_conditions.rb +131 -0
- data/lib/searchlogic/named_scopes/association_ordering.rb +44 -0
- data/lib/searchlogic/named_scopes/conditions.rb +227 -0
- data/lib/searchlogic/named_scopes/or_conditions.rb +141 -0
- data/lib/searchlogic/named_scopes/ordering.rb +48 -0
- data/lib/searchlogic/rails_helpers.rb +76 -0
- data/lib/searchlogic/search.rb +209 -0
- data/rails/init.rb +1 -0
- data/searchlogic.gemspec +86 -0
- data/spec/active_record/consistency_spec.rb +28 -0
- data/spec/core_ext/object_spec.rb +7 -0
- data/spec/core_ext/proc_spec.rb +9 -0
- data/spec/named_scopes/alias_scope_spec.rb +19 -0
- data/spec/named_scopes/association_conditions_spec.rb +188 -0
- data/spec/named_scopes/association_ordering_spec.rb +27 -0
- data/spec/named_scopes/conditions_spec.rb +319 -0
- data/spec/named_scopes/or_conditions_spec.rb +66 -0
- data/spec/named_scopes/ordering_spec.rb +34 -0
- data/spec/search_spec.rb +416 -0
- data/spec/spec_helper.rb +129 -0
- metadata +107 -0
@@ -0,0 +1,101 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module ActiveRecord
|
3
|
+
# Adds methods that give extra information about a classes named scopes.
|
4
|
+
module NamedScopeTools
|
5
|
+
# Retrieves the options passed when creating the respective named scope. Ex:
|
6
|
+
#
|
7
|
+
# named_scope :whatever, :conditions => {:column => value}
|
8
|
+
#
|
9
|
+
# This method will return:
|
10
|
+
#
|
11
|
+
# :conditions => {:column => value}
|
12
|
+
#
|
13
|
+
# ActiveRecord hides this internally in a Proc, so we have to try and pull it out with this
|
14
|
+
# method.
|
15
|
+
def named_scope_options(name)
|
16
|
+
key = scopes.key?(name.to_sym) ? name.to_sym : condition_scope_name(name)
|
17
|
+
|
18
|
+
if key
|
19
|
+
eval("options", scopes[key].binding)
|
20
|
+
else
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# The arity for a named scope's proc is important, because we use the arity
|
26
|
+
# to determine if the condition should be ignored when calling the search method.
|
27
|
+
# If the condition is false and the arity is 0, then we skip it all together. Ex:
|
28
|
+
#
|
29
|
+
# User.named_scope :age_is_4, :conditions => {:age => 4}
|
30
|
+
# User.search(:age_is_4 => false) == User.all
|
31
|
+
# User.search(:age_is_4 => true) == User.all(:conditions => {:age => 4})
|
32
|
+
#
|
33
|
+
# We also use it when trying to "copy" the underlying named scope for association
|
34
|
+
# conditions. This way our aliased scope accepts the same number of parameters for
|
35
|
+
# the underlying scope.
|
36
|
+
def named_scope_arity(name)
|
37
|
+
options = named_scope_options(name)
|
38
|
+
options.respond_to?(:arity) ? options.arity : nil
|
39
|
+
end
|
40
|
+
|
41
|
+
# When searchlogic calls a named_scope on a foreigh model it will execute that scope and then call scope(:find).
|
42
|
+
# When we get these options we want this to be in an exclusive scope, especially if we are calling a condition on
|
43
|
+
# the same originating model:
|
44
|
+
#
|
45
|
+
# Company.users_company_name_equals("name")
|
46
|
+
#
|
47
|
+
# If we aren't in an exclusive scope we will get unexpected results for the :joins option. Lastly, we want the named_scopes
|
48
|
+
# generated by searchlogic to be symbols whenever possible. The reason for this is so that we can allow
|
49
|
+
# ActiveRecord to leverage its joins library that automatically aliases joins if they appear more than once in a query.
|
50
|
+
# If the joins are strings, AtiveRecord can't do anything. Because the code that does this in ActiveRecord is pretty bad
|
51
|
+
# when it comes to being consisitent, searchlogic had to fix this in Searchloigc::ActiveRecord::Consistency. That said,
|
52
|
+
# part of this fix is to normalize joins into strings. We do not want to do this if we are calling scopes on foreigh models.
|
53
|
+
# Only when we are performing an action on it. This is what the searchlogic_delegation thread variable is all about. A
|
54
|
+
# flag to let search logic know not to convert joins to strings.
|
55
|
+
def in_searchlogic_delegation(&block)
|
56
|
+
old = Thread.current["searchlogic_delegation"]
|
57
|
+
Thread.current["searchlogic_delegation"] = true
|
58
|
+
with_exclusive_scope(&block)
|
59
|
+
Thread.current["searchlogic_delegation"] = old
|
60
|
+
end
|
61
|
+
|
62
|
+
# A convenience method for creating inner join sql to that your inner joins
|
63
|
+
# are consistent with how Active Record creates them. Basically a tool for
|
64
|
+
# you to use when writing your own named scopes. This way you know for sure
|
65
|
+
# that duplicate joins will be removed when chaining scopes together that
|
66
|
+
# use the same join.
|
67
|
+
#
|
68
|
+
# Also, don't worry about breaking up the joins or retriving multiple joins.
|
69
|
+
# ActiveRecord will remove dupilicate joins and Searchlogic assists ActiveRecord in
|
70
|
+
# breaking up your joins so that they are unique.
|
71
|
+
def inner_joins(association_name)
|
72
|
+
::ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
|
73
|
+
end
|
74
|
+
|
75
|
+
# A convenience methods to create a join on a polymorphic associations target.
|
76
|
+
# Ex:
|
77
|
+
#
|
78
|
+
# Audit.belong_to :auditable, :polymorphic => true
|
79
|
+
# User.has_many :audits, :as => :auditable
|
80
|
+
#
|
81
|
+
# Audit.inner_polymorphic_join(:user, :as => :auditable) # =>
|
82
|
+
# "INNER JOINER users ON users.id = audits.auditable_id AND audits.auditable_type = 'User'"
|
83
|
+
#
|
84
|
+
# This is used internally by searchlogic to handle accessing conditions on polymorphic associations.
|
85
|
+
def inner_polymorphic_join(target, options = {})
|
86
|
+
options[:on] ||= table_name
|
87
|
+
options[:on_table_name] ||= connection.quote_table_name(options[:on])
|
88
|
+
options[:target_table] ||= connection.quote_table_name(target.to_s.pluralize)
|
89
|
+
options[:as] ||= "owner"
|
90
|
+
postgres = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
91
|
+
"INNER JOIN #{options[:target_table]} ON #{options[:target_table]}.id = #{options[:on_table_name]}.#{options[:as]}_id AND " +
|
92
|
+
"#{options[:on_table_name]}.#{options[:as]}_type = #{postgres ? "E" : ""}'#{target.to_s.camelize}'"
|
93
|
+
end
|
94
|
+
|
95
|
+
# See inner_joins. Does the same thing except creates LEFT OUTER joins.
|
96
|
+
def left_outer_joins(association_name)
|
97
|
+
::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -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,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,131 @@
|
|
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, details[:poly_class])
|
17
|
+
send(name, *args)
|
18
|
+
else
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def association_condition_details(name, last_condition = nil)
|
24
|
+
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 }
|
25
|
+
poly_assocs = reflect_on_all_associations.reject { |assoc| !assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
|
26
|
+
return nil if non_poly_assocs.empty? && poly_assocs.empty?
|
27
|
+
|
28
|
+
name_with_condition = [name, last_condition].compact.join('_')
|
29
|
+
|
30
|
+
association_name = nil
|
31
|
+
poly_type = nil
|
32
|
+
condition = nil
|
33
|
+
|
34
|
+
if name_with_condition.to_s =~ /^(#{non_poly_assocs.collect(&:name).join("|")})_(\w+)$/
|
35
|
+
association_name = $1
|
36
|
+
condition = $2
|
37
|
+
elsif name_with_condition.to_s =~ /^(#{poly_assocs.collect(&:name).join("|")})_(\w+?)_type_(\w+)$/
|
38
|
+
association_name = $1
|
39
|
+
poly_type = $2
|
40
|
+
condition = $3
|
41
|
+
end
|
42
|
+
|
43
|
+
if association_name && condition
|
44
|
+
association = reflect_on_association(association_name.to_sym)
|
45
|
+
klass = poly_type ? poly_type.camelcase.constantize : association.klass
|
46
|
+
if klass.condition?(condition)
|
47
|
+
{:association => association, :poly_class => poly_type && klass, :condition => condition}
|
48
|
+
else
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_association_condition(association, condition_name, args, poly_class = nil)
|
55
|
+
name = [association.name, poly_class && "#{poly_class.name.underscore}_type", condition_name].compact.join("_")
|
56
|
+
named_scope(name, association_condition_options(association, condition_name, args, poly_class))
|
57
|
+
end
|
58
|
+
|
59
|
+
def association_condition_options(association, association_condition, args, poly_class = nil)
|
60
|
+
klass = poly_class ? poly_class : association.klass
|
61
|
+
scope = klass.send(association_condition, *args)
|
62
|
+
scope_options = klass.named_scope_options(association_condition)
|
63
|
+
arity = klass.named_scope_arity(association_condition)
|
64
|
+
|
65
|
+
if !arity || arity == 0
|
66
|
+
# The underlying condition doesn't require any parameters, so let's just create a simple
|
67
|
+
# named scope that is based on a hash.
|
68
|
+
options = {}
|
69
|
+
in_searchlogic_delegation { options = scope.scope(:find) }
|
70
|
+
prepare_named_scope_options(options, association, poly_class)
|
71
|
+
options
|
72
|
+
else
|
73
|
+
proc_args = arity_args(arity)
|
74
|
+
arg_type = (scope_options.respond_to?(:searchlogic_arg_type) && scope_options.searchlogic_arg_type) || :string
|
75
|
+
|
76
|
+
eval <<-"end_eval"
|
77
|
+
searchlogic_lambda(:#{arg_type}) { |#{proc_args.join(",")}|
|
78
|
+
options = {}
|
79
|
+
|
80
|
+
in_searchlogic_delegation do
|
81
|
+
scope = klass.send(association_condition, #{proc_args.join(",")})
|
82
|
+
options = scope.scope(:find) if scope
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
prepare_named_scope_options(options, association, poly_class)
|
87
|
+
options
|
88
|
+
}
|
89
|
+
end_eval
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Used to match the new scopes parameters to the underlying scope. This way we can disguise the
|
94
|
+
# new scope as best as possible instead of taking the easy way out and using *args.
|
95
|
+
def arity_args(arity)
|
96
|
+
args = []
|
97
|
+
if arity > 0
|
98
|
+
arity.times { |i| args << "arg#{i}" }
|
99
|
+
else
|
100
|
+
positive_arity = arity * -1
|
101
|
+
positive_arity.times do |i|
|
102
|
+
if i == (positive_arity - 1)
|
103
|
+
args << "*arg#{i}"
|
104
|
+
else
|
105
|
+
args << "arg#{i}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
args
|
110
|
+
end
|
111
|
+
|
112
|
+
def prepare_named_scope_options(options, association, poly_class = nil)
|
113
|
+
options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
|
114
|
+
|
115
|
+
klass = poly_class || association.klass
|
116
|
+
# sanitize the conditions locally so we get the right table name, otherwise the conditions will be evaluated on the original model
|
117
|
+
options[:conditions] = klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)
|
118
|
+
|
119
|
+
poly_join = poly_class && inner_polymorphic_join(poly_class.name.underscore, :as => association.name)
|
120
|
+
|
121
|
+
if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
|
122
|
+
options[:joins] = [poly_class ? poly_join : inner_joins(association.name), options[:joins]].flatten
|
123
|
+
elsif poly_class
|
124
|
+
options[:joins] = options[:joins].blank? ? poly_join : ([poly_join] + klass.inner_joins(options[:joins]))
|
125
|
+
else
|
126
|
+
options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
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
|
+
named_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,227 @@
|
|
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(method_name)
|
93
|
+
column_name_matcher = column_names.join("|")
|
94
|
+
conditions_matcher = (PRIMARY_CONDITIONS + ALIAS_CONDITIONS).join("|")
|
95
|
+
|
96
|
+
if method_name.to_s =~ /^(#{column_name_matcher})_(#{conditions_matcher})$/
|
97
|
+
{:column => $1, :condition => $2}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def create_condition(column, condition, args)
|
102
|
+
if PRIMARY_CONDITIONS.include?(condition.to_sym)
|
103
|
+
create_primary_condition(column, condition)
|
104
|
+
elsif ALIAS_CONDITIONS.include?(condition.to_sym)
|
105
|
+
create_alias_condition(column, condition, args)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def create_primary_condition(column, condition)
|
110
|
+
column_type = columns_hash[column.to_s].type
|
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) })
|
116
|
+
when /^does_not_equal/
|
117
|
+
scope_options(condition, column_type, "#{table_name}.#{column} != ?")
|
118
|
+
when /^less_than_or_equal_to/
|
119
|
+
scope_options(condition, column_type, "#{table_name}.#{column} <= ?")
|
120
|
+
when /^less_than/
|
121
|
+
scope_options(condition, column_type, "#{table_name}.#{column} < ?")
|
122
|
+
when /^greater_than_or_equal_to/
|
123
|
+
scope_options(condition, column_type, "#{table_name}.#{column} >= ?")
|
124
|
+
when /^greater_than/
|
125
|
+
scope_options(condition, column_type, "#{table_name}.#{column} > ?")
|
126
|
+
when /^like/
|
127
|
+
scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :like)
|
128
|
+
when /^not_like/
|
129
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :like)
|
130
|
+
when /^begins_with/
|
131
|
+
scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :begins_with)
|
132
|
+
when /^not_begin_with/
|
133
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :begins_with)
|
134
|
+
when /^ends_with/
|
135
|
+
scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :ends_with)
|
136
|
+
when /^not_end_with/
|
137
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :ends_with)
|
138
|
+
when "null"
|
139
|
+
{:conditions => "#{table_name}.#{column} IS NULL"}
|
140
|
+
when "not_null"
|
141
|
+
{:conditions => "#{table_name}.#{column} IS NOT NULL"}
|
142
|
+
when "empty"
|
143
|
+
{:conditions => "#{table_name}.#{column} = ''"}
|
144
|
+
when "blank"
|
145
|
+
{:conditions => "#{table_name}.#{column} = '' OR #{table_name}.#{column} IS NULL"}
|
146
|
+
when "not_blank"
|
147
|
+
{:conditions => "#{table_name}.#{column} != '' AND #{table_name}.#{column} IS NOT NULL"}
|
148
|
+
end
|
149
|
+
|
150
|
+
named_scope("#{column}_#{condition}".to_sym, scope_options)
|
151
|
+
end
|
152
|
+
|
153
|
+
# This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions.
|
154
|
+
# Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as
|
155
|
+
# fast as possible, which is why I didn't do the case statement inside of the lambda.
|
156
|
+
def scope_options(condition, column_type, sql, value_modifier = nil)
|
157
|
+
case condition.to_s
|
158
|
+
when /_(any|all)$/
|
159
|
+
searchlogic_lambda(column_type) { |*values|
|
160
|
+
return {} if values.empty?
|
161
|
+
values.flatten!
|
162
|
+
values.collect! { |value| value_with_modifier(value, value_modifier) }
|
163
|
+
|
164
|
+
join = $1 == "any" ? " OR " : " AND "
|
165
|
+
scope_sql = values.collect { |value| sql.is_a?(Proc) ? sql.call(value) : sql }.join(join)
|
166
|
+
|
167
|
+
{:conditions => [scope_sql, *expand_range_bind_variables(values)]}
|
168
|
+
}
|
169
|
+
else
|
170
|
+
searchlogic_lambda(column_type) { |*values|
|
171
|
+
values.collect! { |value| value_with_modifier(value, value_modifier) }
|
172
|
+
|
173
|
+
scope_sql = sql.is_a?(Proc) ? sql.call(*values) : sql
|
174
|
+
|
175
|
+
{:conditions => [scope_sql, *expand_range_bind_variables(values)]}
|
176
|
+
}
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def value_with_modifier(value, modifier)
|
181
|
+
case modifier
|
182
|
+
when :like
|
183
|
+
"%#{value}%"
|
184
|
+
when :begins_with
|
185
|
+
"#{value}%"
|
186
|
+
when :ends_with
|
187
|
+
"%#{value}"
|
188
|
+
else
|
189
|
+
value
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def create_alias_condition(column, condition, args)
|
194
|
+
primary_condition = primary_condition(condition)
|
195
|
+
alias_name = "#{column}_#{condition}"
|
196
|
+
primary_name = "#{column}_#{primary_condition}"
|
197
|
+
send(primary_name, *args) # go back to method_missing and make sure we create the method
|
198
|
+
(class << self; self; end).class_eval { alias_method alias_name, primary_name }
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns the primary condition for the given alias. Ex:
|
202
|
+
#
|
203
|
+
# primary_condition(:gt) => :greater_than
|
204
|
+
def primary_condition(alias_condition)
|
205
|
+
CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns the primary name for any condition on a column. You can pass it
|
209
|
+
# a primary condition, alias condition, etc, and it will return the proper
|
210
|
+
# primary condition name. This helps simply logic throughout Searchlogic. Ex:
|
211
|
+
#
|
212
|
+
# condition_scope_name(:id_gt) => :id_greater_than
|
213
|
+
# condition_scope_name(:id_greater_than) => :id_greater_than
|
214
|
+
def condition_scope_name(name)
|
215
|
+
if details = condition_details(name)
|
216
|
+
if PRIMARY_CONDITIONS.include?(name.to_sym)
|
217
|
+
name
|
218
|
+
else
|
219
|
+
"#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
|
220
|
+
end
|
221
|
+
else
|
222
|
+
nil
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|