joost-searchlogic 2.1.5.2
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/CHANGELOG.rdoc +346 -0
- data/LICENSE +20 -0
- data/README.rdoc +229 -0
- data/Rakefile +42 -0
- data/VERSION.yml +4 -0
- data/init.rb +1 -0
- data/lib/searchlogic.rb +31 -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/associations.rb +131 -0
- data/lib/searchlogic/named_scopes/conditions.rb +215 -0
- data/lib/searchlogic/named_scopes/ordering.rb +53 -0
- data/lib/searchlogic/rails_helpers.rb +69 -0
- data/lib/searchlogic/search.rb +150 -0
- data/rails/init.rb +1 -0
- data/searchlogic.gemspec +73 -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/associations_spec.rb +120 -0
- data/spec/named_scopes/conditions_spec.rb +253 -0
- data/spec/named_scopes/ordering_spec.rb +23 -0
- data/spec/search_spec.rb +288 -0
- data/spec/spec_helper.rb +79 -0
- metadata +96 -0
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
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.email = "bjohnson@binarylogic.com"
|
10
|
+
gem.homepage = "http://github.com/binarylogic/searchlogic"
|
11
|
+
gem.authors = ["Ben Johnson of Binary Logic"]
|
12
|
+
gem.rubyforge_project = "searchlogic"
|
13
|
+
gem.add_dependency "activerecord", ">= 2.0.0"
|
14
|
+
end
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'spec/rake/spectask'
|
20
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
21
|
+
spec.libs << 'lib' << 'spec'
|
22
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
23
|
+
end
|
24
|
+
|
25
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
26
|
+
spec.libs << 'lib' << 'spec'
|
27
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
28
|
+
spec.rcov = true
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task :default => :spec
|
33
|
+
|
34
|
+
begin
|
35
|
+
require 'rake/contrib/sshpublisher'
|
36
|
+
namespace :rubyforge do
|
37
|
+
desc "Release gem to RubyForge"
|
38
|
+
task :release => ["rubyforge:release:gem"]
|
39
|
+
end
|
40
|
+
rescue LoadError
|
41
|
+
puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
42
|
+
end
|
data/VERSION.yml
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "searchlogic"
|
data/lib/searchlogic.rb
ADDED
@@ -0,0 +1,31 @@
|
|
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/associations"
|
7
|
+
require "searchlogic/named_scopes/alias_scope"
|
8
|
+
require "searchlogic/search"
|
9
|
+
|
10
|
+
Proc.send(:include, Searchlogic::CoreExt::Proc)
|
11
|
+
Object.send(:include, Searchlogic::CoreExt::Object)
|
12
|
+
ActiveRecord::Base.extend(Searchlogic::NamedScopes::Conditions)
|
13
|
+
ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering)
|
14
|
+
ActiveRecord::Base.extend(Searchlogic::NamedScopes::Associations)
|
15
|
+
ActiveRecord::Base.extend(Searchlogic::NamedScopes::AliasScope)
|
16
|
+
ActiveRecord::Base.extend(Searchlogic::Search::Implementation)
|
17
|
+
|
18
|
+
# Try to use the search method, if it's available. Thinking sphinx and other plugins
|
19
|
+
# like to use that method as well.
|
20
|
+
if !ActiveRecord::Base.respond_to?(:search)
|
21
|
+
ActiveRecord::Base.class_eval do
|
22
|
+
class << self
|
23
|
+
alias_method :search, :searchlogic
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if defined?(ActionController)
|
29
|
+
require "searchlogic/rails_helpers"
|
30
|
+
ActionController::Base.helper(Searchlogic::RailsHelpers)
|
31
|
+
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,131 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module NamedScopes
|
3
|
+
# Handles dynamically creating named scopes for associations.
|
4
|
+
module Associations
|
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
|
+
elsif details = association_ordering_condition_details(name)
|
47
|
+
create_association_ordering_condition(details[:association], details[:order_as], details[:column], args)
|
48
|
+
send(name, *args)
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def association_condition_details(name)
|
55
|
+
associations = reflect_on_all_associations.collect { |assoc| assoc.name }
|
56
|
+
if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::PRIMARY_CONDITIONS.join("|")})$/
|
57
|
+
{:association => $1, :column => $2, :condition => $3}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def create_association_condition(association_name, column, condition, args)
|
62
|
+
named_scope("#{association_name}_#{column}_#{condition}", association_condition_options(association_name, "#{column}_#{condition}", args))
|
63
|
+
end
|
64
|
+
|
65
|
+
def association_alias_condition_details(name)
|
66
|
+
associations = reflect_on_all_associations.collect { |assoc| assoc.name }
|
67
|
+
if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::ALIAS_CONDITIONS.join("|")})$/
|
68
|
+
{:association => $1, :column => $2, :condition => $3}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def create_association_alias_condition(association, column, condition, args)
|
73
|
+
primary_condition = primary_condition(condition)
|
74
|
+
alias_name = "#{association}_#{column}_#{condition}"
|
75
|
+
primary_name = "#{association}_#{column}_#{primary_condition}"
|
76
|
+
send(primary_name, *args) # go back to method_missing and make sure we create the method
|
77
|
+
(class << self; self; end).class_eval { alias_method alias_name, primary_name }
|
78
|
+
end
|
79
|
+
|
80
|
+
def association_ordering_condition_details(name)
|
81
|
+
associations = reflect_on_all_associations.collect { |assoc| assoc.name }
|
82
|
+
if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
|
83
|
+
{:order_as => $1, :association => $2, :column => $3} unless column_names.include?("#{$2}_#{$3}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_association_ordering_condition(association_name, order_as, column, args)
|
88
|
+
named_scope("#{order_as}_by_#{association_name}_#{column}", association_condition_options(association_name, "#{order_as}_by_#{column}", args))
|
89
|
+
end
|
90
|
+
|
91
|
+
def association_condition_options(association_name, association_condition, args)
|
92
|
+
association = reflect_on_association(association_name.to_sym)
|
93
|
+
scope = association.klass.send(association_condition, *args)
|
94
|
+
scope_options = association.klass.named_scope_options(association_condition)
|
95
|
+
arity = association.klass.named_scope_arity(association_condition)
|
96
|
+
|
97
|
+
if !arity || arity == 0
|
98
|
+
# The underlying condition doesn't require any parameters, so let's just create a simple
|
99
|
+
# named scope that is based on a hash.
|
100
|
+
options = scope.proxy_options
|
101
|
+
options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
|
102
|
+
options
|
103
|
+
else
|
104
|
+
# The underlying condition requires parameters, let's match the parameters it requires
|
105
|
+
# and pass those onto the named scope. We can't use proxy_options because that returns the
|
106
|
+
# result after a value has been passed.
|
107
|
+
proc_args = []
|
108
|
+
if arity > 0
|
109
|
+
arity.times { |i| proc_args << "arg#{i}"}
|
110
|
+
else
|
111
|
+
positive_arity = arity * -1
|
112
|
+
positive_arity.times do |i|
|
113
|
+
if i == (positive_arity - 1)
|
114
|
+
proc_args << "*arg#{i}"
|
115
|
+
else
|
116
|
+
proc_args << "arg#{i}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
eval <<-"end_eval"
|
121
|
+
searchlogic_lambda(:#{scope_options.searchlogic_arg_type}) { |#{proc_args.join(",")}|
|
122
|
+
options = association.klass.named_scope_options(association_condition).call(#{proc_args.join(",")})
|
123
|
+
options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
|
124
|
+
options
|
125
|
+
}
|
126
|
+
end_eval
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module NamedScopes
|
3
|
+
# Handles dynamically creating named scopes for columns.
|
4
|
+
module Conditions
|
5
|
+
COMPARISON_CONDITIONS = {
|
6
|
+
:equals => [:is, :eq],
|
7
|
+
:does_not_equal => [:not_equal_to, :is_not, :not, :ne],
|
8
|
+
:less_than => [:lt, :before],
|
9
|
+
:less_than_or_equal_to => [:lte],
|
10
|
+
:greater_than => [:gt, :after],
|
11
|
+
:greater_than_or_equal_to => [:gte],
|
12
|
+
}
|
13
|
+
|
14
|
+
WILDCARD_CONDITIONS = {
|
15
|
+
:like => [:contains, :includes],
|
16
|
+
:begins_with => [:bw],
|
17
|
+
:ends_with => [:ew],
|
18
|
+
}
|
19
|
+
|
20
|
+
BOOLEAN_CONDITIONS = {
|
21
|
+
:null => [:nil],
|
22
|
+
:empty => []
|
23
|
+
}
|
24
|
+
|
25
|
+
CONDITIONS = {}
|
26
|
+
|
27
|
+
COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases|
|
28
|
+
CONDITIONS[condition] = aliases
|
29
|
+
CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym }
|
30
|
+
CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym }
|
31
|
+
end
|
32
|
+
|
33
|
+
BOOLEAN_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases }
|
34
|
+
|
35
|
+
PRIMARY_CONDITIONS = CONDITIONS.keys
|
36
|
+
ALIAS_CONDITIONS = CONDITIONS.values.flatten
|
37
|
+
|
38
|
+
# Retrieves the options passed when creating the respective named scope. Ex:
|
39
|
+
#
|
40
|
+
# named_scope :whatever, :conditions => {:column => value}
|
41
|
+
#
|
42
|
+
# This method will return:
|
43
|
+
#
|
44
|
+
# :conditions => {:column => value}
|
45
|
+
#
|
46
|
+
# ActiveRecord hides this internally, so we have to try and pull it out with this
|
47
|
+
# method.
|
48
|
+
def named_scope_options(name)
|
49
|
+
key = scopes.key?(name.to_sym) ? name.to_sym : primary_condition_name(name)
|
50
|
+
|
51
|
+
if key
|
52
|
+
eval("options", scopes[key])
|
53
|
+
else
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# The arity for a named scope's proc is important, because we use the arity
|
59
|
+
# to determine if the condition should be ignored when calling the search method.
|
60
|
+
# If the condition is false and the arity is 0, then we skip it all together. Ex:
|
61
|
+
#
|
62
|
+
# User.named_scope :age_is_4, :conditions => {:age => 4}
|
63
|
+
# User.search(:age_is_4 => false) == User.all
|
64
|
+
# User.search(:age_is_4 => true) == User.all(:conditions => {:age => 4})
|
65
|
+
#
|
66
|
+
# We also use it when trying to "copy" the underlying named scope for association
|
67
|
+
# conditions.
|
68
|
+
def named_scope_arity(name)
|
69
|
+
options = named_scope_options(name)
|
70
|
+
options.respond_to?(:arity) ? options.arity : nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns the primary condition for the given alias. Ex:
|
74
|
+
#
|
75
|
+
# primary_condition(:gt) => :greater_than
|
76
|
+
def primary_condition(alias_condition)
|
77
|
+
CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the primary name for any condition on a column. You can pass it
|
81
|
+
# a primary condition, alias condition, etc, and it will return the proper
|
82
|
+
# primary condition name. This helps simply logic throughout Searchlogic. Ex:
|
83
|
+
#
|
84
|
+
# primary_condition_name(:id_gt) => :id_greater_than
|
85
|
+
# primary_condition_name(:id_greater_than) => :id_greater_than
|
86
|
+
def primary_condition_name(name)
|
87
|
+
if primary_condition?(name)
|
88
|
+
name.to_sym
|
89
|
+
elsif details = alias_condition_details(name)
|
90
|
+
"#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
|
91
|
+
else
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Is the name of the method a valid condition that can be dynamically created?
|
97
|
+
def condition?(name)
|
98
|
+
primary_condition?(name) || alias_condition?(name)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Is the name of the method a valid condition that can be dynamically created,
|
102
|
+
# AND is it a primary condition (not an alias). "greater_than" not "gt".
|
103
|
+
def primary_condition?(name)
|
104
|
+
!primary_condition_details(name).nil?
|
105
|
+
end
|
106
|
+
|
107
|
+
# Is the name of the method a valid condition that can be dynamically created,
|
108
|
+
# AND is it an alias condition. "gt" not "greater_than".
|
109
|
+
def alias_condition?(name)
|
110
|
+
!alias_condition_details(name).nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
def method_missing(name, *args, &block)
|
115
|
+
if details = primary_condition_details(name)
|
116
|
+
create_primary_condition(details[:column], details[:condition])
|
117
|
+
send(name, *args)
|
118
|
+
elsif details = alias_condition_details(name)
|
119
|
+
create_alias_condition(details[:column], details[:condition], args)
|
120
|
+
send(name, *args)
|
121
|
+
else
|
122
|
+
super
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def primary_condition_details(name)
|
127
|
+
if name.to_s =~ /^(#{column_names.join("|")})_(#{PRIMARY_CONDITIONS.join("|")})$/
|
128
|
+
{:column => $1, :condition => $2}
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def create_primary_condition(column, condition)
|
133
|
+
column_type = columns_hash[column.to_s].type
|
134
|
+
scope_options = case condition.to_s
|
135
|
+
when /^equals/
|
136
|
+
scope_options(condition, column_type, "#{table_name}.#{column} = ?")
|
137
|
+
when /^does_not_equal/
|
138
|
+
scope_options(condition, column_type, "#{table_name}.#{column} != ?")
|
139
|
+
when /^less_than_or_equal_to/
|
140
|
+
scope_options(condition, column_type, "#{table_name}.#{column} <= ?")
|
141
|
+
when /^less_than/
|
142
|
+
scope_options(condition, column_type, "#{table_name}.#{column} < ?")
|
143
|
+
when /^greater_than_or_equal_to/
|
144
|
+
scope_options(condition, column_type, "#{table_name}.#{column} >= ?")
|
145
|
+
when /^greater_than/
|
146
|
+
scope_options(condition, column_type, "#{table_name}.#{column} > ?")
|
147
|
+
when /^like/
|
148
|
+
scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :like)
|
149
|
+
when /^begins_with/
|
150
|
+
scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :begins_with)
|
151
|
+
when /^ends_with/
|
152
|
+
scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :ends_with)
|
153
|
+
when "null"
|
154
|
+
{:conditions => "#{table_name}.#{column} IS NULL"}
|
155
|
+
when "empty"
|
156
|
+
{:conditions => "#{table_name}.#{column} = ''"}
|
157
|
+
end
|
158
|
+
|
159
|
+
named_scope("#{column}_#{condition}".to_sym, scope_options)
|
160
|
+
end
|
161
|
+
|
162
|
+
# This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions.
|
163
|
+
# Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as
|
164
|
+
# fast as possible, which is why I didn't do the case statement inside of the lambda.
|
165
|
+
def scope_options(condition, column_type, sql, value_modifier = nil)
|
166
|
+
case condition.to_s
|
167
|
+
when /_(any|all)$/
|
168
|
+
searchlogic_lambda(column_type) { |*values|
|
169
|
+
return {} if values.empty?
|
170
|
+
values = values.flatten
|
171
|
+
|
172
|
+
values_to_sub = nil
|
173
|
+
if value_modifier.nil?
|
174
|
+
values_to_sub = values
|
175
|
+
else
|
176
|
+
values_to_sub = values.collect { |value| value_with_modifier(value, value_modifier) }
|
177
|
+
end
|
178
|
+
|
179
|
+
join = $1 == "any" ? " OR " : " AND "
|
180
|
+
{:conditions => [values.collect { |value| sql }.join(join), *values_to_sub]}
|
181
|
+
}
|
182
|
+
else
|
183
|
+
searchlogic_lambda(column_type) { |value| {:conditions => [sql, value_with_modifier(value, value_modifier)]} }
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def value_with_modifier(value, modifier)
|
188
|
+
case modifier
|
189
|
+
when :like
|
190
|
+
"%#{value}%"
|
191
|
+
when :begins_with
|
192
|
+
"#{value}%"
|
193
|
+
when :ends_with
|
194
|
+
"%#{value}"
|
195
|
+
else
|
196
|
+
value
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def alias_condition_details(name)
|
201
|
+
if name.to_s =~ /^(#{column_names.join("|")})_(#{ALIAS_CONDITIONS.join("|")})$/
|
202
|
+
{:column => $1, :condition => $2}
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def create_alias_condition(column, condition, args)
|
207
|
+
primary_condition = primary_condition(condition)
|
208
|
+
alias_name = "#{column}_#{condition}"
|
209
|
+
primary_name = "#{column}_#{primary_condition}"
|
210
|
+
send(primary_name, *args) # go back to method_missing and make sure we create the method
|
211
|
+
(class << self; self; end).class_eval { alias_method alias_name, primary_name }
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|