skanev-searchlogic 2.1.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/CHANGELOG.rdoc +361 -0
- data/LICENSE +20 -0
- data/README.rdoc +226 -0
- data/Rakefile +43 -0
- data/VERSION.yml +4 -0
- data/init.rb +1 -0
- data/lib/searchlogic.rb +33 -0
- data/lib/searchlogic/active_record_consistency.rb +27 -0
- data/lib/searchlogic/core_ext/object.rb +39 -0
- data/lib/searchlogic/core_ext/proc.rb +11 -0
- data/lib/searchlogic/named_scopes/alias_scope.rb +63 -0
- data/lib/searchlogic/named_scopes/association_conditions.rb +117 -0
- data/lib/searchlogic/named_scopes/association_ordering.rb +32 -0
- data/lib/searchlogic/named_scopes/conditions.rb +232 -0
- data/lib/searchlogic/named_scopes/ordering.rb +53 -0
- data/lib/searchlogic/rails_helpers.rb +69 -0
- data/lib/searchlogic/search.rb +151 -0
- data/rails/init.rb +1 -0
- data/searchlogic.gemspec +77 -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 +15 -0
- data/spec/named_scopes/association_conditions_spec.rb +104 -0
- data/spec/named_scopes/association_ordering_spec.rb +23 -0
- data/spec/named_scopes/conditions_spec.rb +277 -0
- data/spec/named_scopes/ordering_spec.rb +27 -0
- data/spec/search_spec.rb +295 -0
- data/spec/spec_helper.rb +83 -0
- metadata +100 -0
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "searchlogic"
|
8
|
+
gem.summary = "Searchlogic provides common named scopes and object based searching for ActiveRecord."
|
9
|
+
gem.description = "Searchlogic provides common named scopes and object based searching for ActiveRecord."
|
10
|
+
gem.email = "bjohnson@binarylogic.com"
|
11
|
+
gem.homepage = "http://github.com/binarylogic/searchlogic"
|
12
|
+
gem.authors = ["Ben Johnson of Binary Logic"]
|
13
|
+
gem.rubyforge_project = "searchlogic"
|
14
|
+
gem.add_dependency "activerecord", ">= 2.0.0"
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'spec/rake/spectask'
|
21
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
22
|
+
spec.libs << 'lib' << 'spec'
|
23
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
24
|
+
end
|
25
|
+
|
26
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
27
|
+
spec.libs << 'lib' << 'spec'
|
28
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
29
|
+
spec.rcov = true
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
task :default => :spec
|
34
|
+
|
35
|
+
begin
|
36
|
+
require 'rake/contrib/sshpublisher'
|
37
|
+
namespace :rubyforge do
|
38
|
+
desc "Release gem to RubyForge"
|
39
|
+
task :release => ["rubyforge:release:gem"]
|
40
|
+
end
|
41
|
+
rescue LoadError
|
42
|
+
puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
43
|
+
end
|
data/VERSION.yml
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "searchlogic"
|
data/lib/searchlogic.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "searchlogic/core_ext/proc"
|
2
|
+
require "searchlogic/core_ext/object"
|
3
|
+
require "searchlogic/active_record_consistency"
|
4
|
+
require "searchlogic/named_scopes/conditions"
|
5
|
+
require "searchlogic/named_scopes/ordering"
|
6
|
+
require "searchlogic/named_scopes/association_conditions"
|
7
|
+
require "searchlogic/named_scopes/association_ordering"
|
8
|
+
require "searchlogic/named_scopes/alias_scope"
|
9
|
+
require "searchlogic/search"
|
10
|
+
|
11
|
+
Proc.send(:include, Searchlogic::CoreExt::Proc)
|
12
|
+
Object.send(:include, Searchlogic::CoreExt::Object)
|
13
|
+
ActiveRecord::Base.extend(Searchlogic::NamedScopes::Conditions)
|
14
|
+
ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering)
|
15
|
+
ActiveRecord::Base.extend(Searchlogic::NamedScopes::AssociationConditions)
|
16
|
+
ActiveRecord::Base.extend(Searchlogic::NamedScopes::AssociationOrdering)
|
17
|
+
ActiveRecord::Base.extend(Searchlogic::NamedScopes::AliasScope)
|
18
|
+
ActiveRecord::Base.extend(Searchlogic::Search::Implementation)
|
19
|
+
|
20
|
+
# Try to use the search method, if it's available. Thinking sphinx and other plugins
|
21
|
+
# like to use that method as well.
|
22
|
+
if !ActiveRecord::Base.respond_to?(:search)
|
23
|
+
ActiveRecord::Base.class_eval do
|
24
|
+
class << self
|
25
|
+
alias_method :search, :searchlogic
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if defined?(ActionController)
|
31
|
+
require "searchlogic/rails_helpers"
|
32
|
+
ActionController::Base.helper(Searchlogic::RailsHelpers)
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
# Active Record is pretty inconsistent with how their SQL is constructed. This
|
3
|
+
# method attempts to close the gap between the various inconsistencies.
|
4
|
+
module ActiveRecordConsistency
|
5
|
+
def self.included(klass)
|
6
|
+
klass.class_eval do
|
7
|
+
alias_method_chain :merge_joins, :searchlogic
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# In AR multiple joins are sometimes in a single join query, and other time they
|
12
|
+
# are not. The merge_joins method in AR should account for this, but it doesn't.
|
13
|
+
# This fixes that problem.
|
14
|
+
def merge_joins_with_searchlogic(*args)
|
15
|
+
joins = merge_joins_without_searchlogic(*args)
|
16
|
+
joins.collect { |j| j.is_a?(String) ? j.split(" ") : j }.flatten.uniq
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ActiveRecord # :nodoc: all
|
22
|
+
class Base
|
23
|
+
class << self
|
24
|
+
include Searchlogic::ActiveRecordConsistency
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
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 are want a string.
|
31
|
+
# If you want something else, you need to specify it as I did in the above example.
|
32
|
+
def searchlogic_lambda(type = :string, &block)
|
33
|
+
proc = lambda(&block)
|
34
|
+
proc.searchlogic_arg_type = type
|
35
|
+
proc
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,63 @@
|
|
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, while at the same time letting
|
5
|
+
# Searchlogic know that this is a safe method.
|
6
|
+
module AliasScope
|
7
|
+
# The searchlogic Search class takes a hash and chains the values together as named scopes.
|
8
|
+
# For security reasons the only hash keys that are allowed must be mapped to named scopes.
|
9
|
+
# You can not pass the name of a class method and expect that to be called. In some instances
|
10
|
+
# you might create a class method that essentially aliases a named scope or represents a
|
11
|
+
# named scope procedure. Ex:
|
12
|
+
#
|
13
|
+
# User.named_scope :teenager, :conditions => ["age >= ? AND age <= ?", 13, 19]
|
14
|
+
#
|
15
|
+
# This is obviously a very basic example, but there is logic that is duplicated here. For
|
16
|
+
# more complicated named scopes this might make more sense, but to make my point you could
|
17
|
+
# do something like this instead
|
18
|
+
#
|
19
|
+
# class User
|
20
|
+
# def teenager
|
21
|
+
# age_gte(13).age_lte(19)
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# As I stated above, you could not use this method with the Searchlogic::Search class because
|
26
|
+
# there is no way to tell that this is actually a named scope. Instead, Searchlogic lets you
|
27
|
+
# do something like this:
|
28
|
+
#
|
29
|
+
# User.alias_scope :teenager, lambda { age_gte(13).age_lte(19) }
|
30
|
+
#
|
31
|
+
# It fits in better, at the same time Searchlogic will know this is an acceptable named scope.
|
32
|
+
def alias_scope(name, options = nil)
|
33
|
+
alias_scopes[name.to_sym] = options
|
34
|
+
(class << self; self end).instance_eval do
|
35
|
+
define_method name do |*args|
|
36
|
+
case options
|
37
|
+
when Symbol
|
38
|
+
send(options)
|
39
|
+
else
|
40
|
+
options.call(*args)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def alias_scopes # :nodoc:
|
47
|
+
@alias_scopes ||= {}
|
48
|
+
end
|
49
|
+
|
50
|
+
def alias_scope?(name) # :nodoc:
|
51
|
+
alias_scopes.key?(name.to_sym)
|
52
|
+
end
|
53
|
+
|
54
|
+
def condition?(name) # :nodoc:
|
55
|
+
super || alias_scope?(name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def named_scope_options(name) # :nodoc:
|
59
|
+
super || alias_scopes[name.to_sym]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module NamedScopes
|
3
|
+
# Handles dynamically creating named scopes for associations.
|
4
|
+
module AssociationConditions
|
5
|
+
def condition?(name) # :nodoc:
|
6
|
+
super || association_condition?(name) || association_alias_condition?(name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def primary_condition_name(name) # :nodoc:
|
10
|
+
if result = super
|
11
|
+
result
|
12
|
+
elsif association_condition?(name)
|
13
|
+
name.to_sym
|
14
|
+
elsif details = association_alias_condition_details(name)
|
15
|
+
"#{details[:association]}_#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
|
16
|
+
else
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Is the name of the method a valid name for an association condition?
|
22
|
+
def association_condition?(name)
|
23
|
+
!association_condition_details(name).nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Is the ane of the method a valie name for an association alias condition?
|
27
|
+
# An alias being "gt" for "greater_than", etc.
|
28
|
+
def association_alias_condition?(name)
|
29
|
+
!association_alias_condition_details(name).nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
# A convenience method for creating inner join sql to that your inner joins
|
33
|
+
# are consistent with how Active Record creates them.
|
34
|
+
def inner_joins(association_name)
|
35
|
+
ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def method_missing(name, *args, &block)
|
40
|
+
if details = association_condition_details(name)
|
41
|
+
create_association_condition(details[:association], details[:column], details[:condition], args)
|
42
|
+
send(name, *args)
|
43
|
+
elsif details = association_alias_condition_details(name)
|
44
|
+
create_association_alias_condition(details[:association], details[:column], details[:condition], args)
|
45
|
+
send(name, *args)
|
46
|
+
else
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def association_condition_details(name)
|
52
|
+
associations = reflect_on_all_associations.collect { |assoc| assoc.name }
|
53
|
+
if !local_condition?(name) && name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::PRIMARY_CONDITIONS.join("|")})$/
|
54
|
+
{:association => $1, :column => $2, :condition => $3}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_association_condition(association_name, column, condition, args)
|
59
|
+
named_scope("#{association_name}_#{column}_#{condition}", association_condition_options(association_name, "#{column}_#{condition}", args))
|
60
|
+
end
|
61
|
+
|
62
|
+
def association_alias_condition_details(name)
|
63
|
+
associations = reflect_on_all_associations.collect { |assoc| assoc.name }
|
64
|
+
if !local_condition?(name) && name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::ALIAS_CONDITIONS.join("|")})$/
|
65
|
+
{:association => $1, :column => $2, :condition => $3}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_association_alias_condition(association, column, condition, args)
|
70
|
+
primary_condition = primary_condition(condition)
|
71
|
+
alias_name = "#{association}_#{column}_#{condition}"
|
72
|
+
primary_name = "#{association}_#{column}_#{primary_condition}"
|
73
|
+
send(primary_name, *args) # go back to method_missing and make sure we create the method
|
74
|
+
(class << self; self; end).class_eval { alias_method alias_name, primary_name }
|
75
|
+
end
|
76
|
+
|
77
|
+
def association_condition_options(association_name, association_condition, args)
|
78
|
+
association = reflect_on_association(association_name.to_sym)
|
79
|
+
scope = association.klass.send(association_condition, *args)
|
80
|
+
scope_options = association.klass.named_scope_options(association_condition)
|
81
|
+
arity = association.klass.named_scope_arity(association_condition)
|
82
|
+
|
83
|
+
if !arity || arity == 0
|
84
|
+
# The underlying condition doesn't require any parameters, so let's just create a simple
|
85
|
+
# named scope that is based on a hash.
|
86
|
+
options = scope.proxy_options
|
87
|
+
options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
|
88
|
+
options
|
89
|
+
else
|
90
|
+
# The underlying condition requires parameters, let's match the parameters it requires
|
91
|
+
# and pass those onto the named scope. We can't use proxy_options because that returns the
|
92
|
+
# result after a value has been passed.
|
93
|
+
proc_args = []
|
94
|
+
if arity > 0
|
95
|
+
arity.times { |i| proc_args << "arg#{i}"}
|
96
|
+
else
|
97
|
+
positive_arity = arity * -1
|
98
|
+
positive_arity.times do |i|
|
99
|
+
if i == (positive_arity - 1)
|
100
|
+
proc_args << "*arg#{i}"
|
101
|
+
else
|
102
|
+
proc_args << "arg#{i}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
eval <<-"end_eval"
|
107
|
+
searchlogic_lambda(:#{scope_options.searchlogic_arg_type}) { |#{proc_args.join(",")}|
|
108
|
+
options = association.klass.named_scope_options(association_condition).call(#{proc_args.join(",")})
|
109
|
+
options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
|
110
|
+
options
|
111
|
+
}
|
112
|
+
end_eval
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module NamedScopes
|
3
|
+
# Handles dynamically creating named scopes for associations.
|
4
|
+
module AssociationOrdering
|
5
|
+
|
6
|
+
def association_ordering_condition?(name)
|
7
|
+
!association_ordering_condition_details(name).nil?
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def method_missing(name, *args, &block)
|
12
|
+
if details = association_ordering_condition_details(name)
|
13
|
+
create_association_ordering_condition(details[:association], details[:order_as], details[:column], args)
|
14
|
+
send(name, *args)
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def association_ordering_condition_details(name)
|
21
|
+
associations = reflect_on_all_associations.collect { |assoc| assoc.name }
|
22
|
+
if !local_condition?(name) && name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
|
23
|
+
{:order_as => $1, :association => $2, :column => $3}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_association_ordering_condition(association_name, order_as, column, args)
|
28
|
+
named_scope("#{order_as}_by_#{association_name}_#{column}", association_condition_options(association_name, "#{order_as}_by_#{column}", args))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,232 @@
|
|
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
|
+
:not_like => [],
|
17
|
+
:begins_with => [:bw],
|
18
|
+
:not_begin_with => [:does_not_begin_with],
|
19
|
+
:ends_with => [:ew],
|
20
|
+
:not_end_with => [:does_not_end_with]
|
21
|
+
}
|
22
|
+
|
23
|
+
BOOLEAN_CONDITIONS = {
|
24
|
+
:null => [:nil],
|
25
|
+
:not_null => [:not_nil],
|
26
|
+
:empty => []
|
27
|
+
}
|
28
|
+
|
29
|
+
CONDITIONS = {}
|
30
|
+
|
31
|
+
COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases|
|
32
|
+
CONDITIONS[condition] = aliases
|
33
|
+
CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym }
|
34
|
+
CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym }
|
35
|
+
end
|
36
|
+
|
37
|
+
BOOLEAN_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases }
|
38
|
+
|
39
|
+
PRIMARY_CONDITIONS = CONDITIONS.keys
|
40
|
+
ALIAS_CONDITIONS = CONDITIONS.values.flatten
|
41
|
+
|
42
|
+
# Retrieves the options passed when creating the respective named scope. Ex:
|
43
|
+
#
|
44
|
+
# named_scope :whatever, :conditions => {:column => value}
|
45
|
+
#
|
46
|
+
# This method will return:
|
47
|
+
#
|
48
|
+
# :conditions => {:column => value}
|
49
|
+
#
|
50
|
+
# ActiveRecord hides this internally, so we have to try and pull it out with this
|
51
|
+
# method.
|
52
|
+
def named_scope_options(name)
|
53
|
+
key = scopes.key?(name.to_sym) ? name.to_sym : primary_condition_name(name)
|
54
|
+
|
55
|
+
if key
|
56
|
+
eval("options", scopes[key])
|
57
|
+
else
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# The arity for a named scope's proc is important, because we use the arity
|
63
|
+
# to determine if the condition should be ignored when calling the search method.
|
64
|
+
# If the condition is false and the arity is 0, then we skip it all together. Ex:
|
65
|
+
#
|
66
|
+
# User.named_scope :age_is_4, :conditions => {:age => 4}
|
67
|
+
# User.search(:age_is_4 => false) == User.all
|
68
|
+
# User.search(:age_is_4 => true) == User.all(:conditions => {:age => 4})
|
69
|
+
#
|
70
|
+
# We also use it when trying to "copy" the underlying named scope for association
|
71
|
+
# conditions.
|
72
|
+
def named_scope_arity(name)
|
73
|
+
options = named_scope_options(name)
|
74
|
+
options.respond_to?(:arity) ? options.arity : nil
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns the primary condition for the given alias. Ex:
|
78
|
+
#
|
79
|
+
# primary_condition(:gt) => :greater_than
|
80
|
+
def primary_condition(alias_condition)
|
81
|
+
CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns the primary name for any condition on a column. You can pass it
|
85
|
+
# a primary condition, alias condition, etc, and it will return the proper
|
86
|
+
# primary condition name. This helps simply logic throughout Searchlogic. Ex:
|
87
|
+
#
|
88
|
+
# primary_condition_name(:id_gt) => :id_greater_than
|
89
|
+
# primary_condition_name(:id_greater_than) => :id_greater_than
|
90
|
+
def primary_condition_name(name)
|
91
|
+
if primary_condition?(name)
|
92
|
+
name.to_sym
|
93
|
+
elsif details = alias_condition_details(name)
|
94
|
+
"#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
|
95
|
+
else
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Is the name of the method a valid condition that can be dynamically created?
|
101
|
+
def condition?(name)
|
102
|
+
local_condition?(name)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Is the condition for a local column, not an association
|
106
|
+
def local_condition?(name)
|
107
|
+
primary_condition?(name) || alias_condition?(name)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Is the name of the method a valid condition that can be dynamically created,
|
111
|
+
# AND is it a primary condition (not an alias). "greater_than" not "gt".
|
112
|
+
def primary_condition?(name)
|
113
|
+
!primary_condition_details(name).nil?
|
114
|
+
end
|
115
|
+
|
116
|
+
# Is the name of the method a valid condition that can be dynamically created,
|
117
|
+
# AND is it an alias condition. "gt" not "greater_than".
|
118
|
+
def alias_condition?(name)
|
119
|
+
!alias_condition_details(name).nil?
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
def method_missing(name, *args, &block)
|
124
|
+
if details = primary_condition_details(name)
|
125
|
+
create_primary_condition(details[:column], details[:condition])
|
126
|
+
send(name, *args)
|
127
|
+
elsif details = alias_condition_details(name)
|
128
|
+
create_alias_condition(details[:column], details[:condition], args)
|
129
|
+
send(name, *args)
|
130
|
+
else
|
131
|
+
super
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def primary_condition_details(name)
|
136
|
+
if name.to_s =~ /^(#{column_names.join("|")})_(#{PRIMARY_CONDITIONS.join("|")})$/
|
137
|
+
{:column => $1, :condition => $2}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def create_primary_condition(column, condition)
|
142
|
+
column_type = columns_hash[column.to_s].type
|
143
|
+
scope_options = case condition.to_s
|
144
|
+
when /^equals/
|
145
|
+
scope_options(condition, column_type, "#{table_name}.#{column} = ?")
|
146
|
+
when /^does_not_equal/
|
147
|
+
scope_options(condition, column_type, "#{table_name}.#{column} != ?")
|
148
|
+
when /^less_than_or_equal_to/
|
149
|
+
scope_options(condition, column_type, "#{table_name}.#{column} <= ?")
|
150
|
+
when /^less_than/
|
151
|
+
scope_options(condition, column_type, "#{table_name}.#{column} < ?")
|
152
|
+
when /^greater_than_or_equal_to/
|
153
|
+
scope_options(condition, column_type, "#{table_name}.#{column} >= ?")
|
154
|
+
when /^greater_than/
|
155
|
+
scope_options(condition, column_type, "#{table_name}.#{column} > ?")
|
156
|
+
when /^like/
|
157
|
+
scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :like)
|
158
|
+
when /^not_like/
|
159
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT LIKE ?", :like)
|
160
|
+
when /^begins_with/
|
161
|
+
scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :begins_with)
|
162
|
+
when /^not_begin_with/
|
163
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT LIKE ?", :begins_with)
|
164
|
+
when /^ends_with/
|
165
|
+
scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :ends_with)
|
166
|
+
when /^not_end_with/
|
167
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT LIKE ?", :ends_with)
|
168
|
+
when "null"
|
169
|
+
{:conditions => "#{table_name}.#{column} IS NULL"}
|
170
|
+
when "not_null"
|
171
|
+
{:conditions => "#{table_name}.#{column} IS NOT NULL"}
|
172
|
+
when "empty"
|
173
|
+
{:conditions => "#{table_name}.#{column} = ''"}
|
174
|
+
end
|
175
|
+
|
176
|
+
named_scope("#{column}_#{condition}".to_sym, scope_options)
|
177
|
+
end
|
178
|
+
|
179
|
+
# This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions.
|
180
|
+
# Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as
|
181
|
+
# fast as possible, which is why I didn't do the case statement inside of the lambda.
|
182
|
+
def scope_options(condition, column_type, sql, value_modifier = nil)
|
183
|
+
case condition.to_s
|
184
|
+
when /_(any|all)$/
|
185
|
+
searchlogic_lambda(column_type) { |*values|
|
186
|
+
return {} if values.empty?
|
187
|
+
values = values.flatten
|
188
|
+
|
189
|
+
values_to_sub = nil
|
190
|
+
if value_modifier.nil?
|
191
|
+
values_to_sub = values
|
192
|
+
else
|
193
|
+
values_to_sub = values.collect { |value| value_with_modifier(value, value_modifier) }
|
194
|
+
end
|
195
|
+
|
196
|
+
join = $1 == "any" ? " OR " : " AND "
|
197
|
+
{:conditions => [values.collect { |value| sql }.join(join), *values_to_sub]}
|
198
|
+
}
|
199
|
+
else
|
200
|
+
searchlogic_lambda(column_type) { |value| {:conditions => [sql, value_with_modifier(value, value_modifier)]} }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def value_with_modifier(value, modifier)
|
205
|
+
case modifier
|
206
|
+
when :like
|
207
|
+
"%#{value}%"
|
208
|
+
when :begins_with
|
209
|
+
"#{value}%"
|
210
|
+
when :ends_with
|
211
|
+
"%#{value}"
|
212
|
+
else
|
213
|
+
value
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def alias_condition_details(name)
|
218
|
+
if name.to_s =~ /^(#{column_names.join("|")})_(#{ALIAS_CONDITIONS.join("|")})$/
|
219
|
+
{:column => $1, :condition => $2}
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def create_alias_condition(column, condition, args)
|
224
|
+
primary_condition = primary_condition(condition)
|
225
|
+
alias_name = "#{column}_#{condition}"
|
226
|
+
primary_name = "#{column}_#{primary_condition}"
|
227
|
+
send(primary_name, *args) # go back to method_missing and make sure we create the method
|
228
|
+
(class << self; self; end).class_eval { alias_method alias_name, primary_name }
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|