searchlogic 2.2.3 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,8 @@
1
+ == 2.3.0 released 2009-08-22
2
+
3
+ * Thanks to laserlemon for support of ranges and arrays in the equals condition.
4
+ * Added feature to combine condition with 'or'.
5
+
1
6
  == 2.2.3 released 2009-07-31
2
7
 
3
8
  * Fixed bug when an associations named scope joins is a string or an array of strings, the joins we add in automatically should also be a string, not a symbol.
data/README.rdoc CHANGED
@@ -1,17 +1,17 @@
1
1
  = Searchlogic
2
2
 
3
- Searchlogic provides common named scopes and object based searching for ActiveRecord.
3
+ Searchlogic provides tools that make using ActiveRecord named scopes easier and less repetitive. It helps keep your code DRY, clean, and simple.
4
4
 
5
5
  == Helpful links
6
6
 
7
7
  * <b>Documentation:</b> http://rdoc.info/projects/binarylogic/searchlogic
8
8
  * <b>Repository:</b> http://github.com/binarylogic/searchlogic/tree/master
9
- * <b>Bugs / feature suggestions:</b> http://binarylogic.lighthouseapp.com/projects/16601-searchlogic
9
+ * <b>Issues:</b> http://github.com/binarylogic/searchlogic/issues
10
10
  * <b>Google group:</b> http://groups.google.com/group/searchlogic
11
11
 
12
12
  <b>Before contacting me directly, please read:</b>
13
13
 
14
- If you find a bug or a problem please post it on lighthouse. If you need help with something, please use google groups. I check both regularly and get emails when anything happens, so that is the best place to get help. This also benefits other people in the future with the same questions / problems. Thank you.
14
+ If you find a bug or a problem please post it in the issues section. If you need help with something, please use google groups. I check both regularly and get emails when anything happens, so that is the best place to get help. This also benefits other people in the future with the same questions / problems. Thank you.
15
15
 
16
16
  == Install & use
17
17
 
@@ -40,35 +40,48 @@ Instead of explaining what Searchlogic can do, let me show you. Let's start at t
40
40
 
41
41
  # Searchlogic gives you a bunch of named scopes for free:
42
42
  User.username_equals("bjohnson")
43
+ User.username_equals(["bjohnson", "thunt"])
44
+ User.username_equals("a".."b")
43
45
  User.username_does_not_equal("bjohnson")
44
46
  User.username_begins_with("bjohnson")
47
+ User.username_not_begin_with("bjohnson")
45
48
  User.username_like("bjohnson")
49
+ User.username_not_like("bjohnson")
46
50
  User.username_ends_with("bjohnson")
51
+ User.username_not_end_with("bjohnson")
47
52
  User.age_greater_than(20)
48
53
  User.age_greater_than_or_equal_to(20)
49
54
  User.age_less_than(20)
50
55
  User.age_less_than_or_equal_to(20)
51
56
  User.username_null
57
+ User.username_not_null
52
58
  User.username_blank
53
-
54
- # You can also order by columns
55
- User.ascend_by_username
56
- User.descend_by_username
57
- User.order("ascend_by_username")
58
59
 
59
60
  Any named scope Searchlogic creates is dynamic and created via method_missing. Meaning it will only create what you need. Also, keep in mind, these are just named scopes, you can chain them, call methods off of them, etc:
60
61
 
61
- scope = User.username_like("bjohnson").age_greater_than(20).ascend_by_username
62
+ scope = User.username_like("bjohnson").age_greater_than(20).id_less_than(55)
62
63
  scope.all
63
64
  scope.first
64
65
  scope.count
65
66
  # etc...
66
67
 
67
- That's all pretty standard, but here's where Searchlogic starts to get interesting...
68
+ For a complete list of conditions please see the constants in Searchlogic::NamedScopes::Conditions.
69
+
70
+ == Use condition aliases
71
+
72
+ Typing out 'greater_than_or_equal_to' is not fun. Instead Searchlogic provides various aliases for the conditions. For a complete list please see Searchlogic::NamedScopes::Conditions. But they are pretty straightforward:
73
+
74
+ User.username_is(10)
75
+ User.username_eq(10)
76
+ User.id_lt(10)
77
+ User.id_lte(10)
78
+ # etc...
79
+
80
+ == Search using scopes in associated classes
68
81
 
69
- == Search using conditions on associated columns
82
+ This is my favorite part of Searchlogic. You can dynamically call scopes on associated classes and Searchlogic will take care of creating the necessary joins for you. This is REALY nice for keeping your code DRY. The best way to explain this is to show you:
70
83
 
71
- You also get named scopes for any of your associations:
84
+ Let's take some basic scopes that Searchlogic provides:
72
85
 
73
86
  # We have the following relationships
74
87
  User.has_many :orders
@@ -83,7 +96,17 @@ You also get named scopes for any of your associations:
83
96
  User.ascend_by_order_total
84
97
  User.descend_by_orders_line_items_price
85
98
 
86
- Again these are just named scopes. You can chain them together, call methods off of them, etc. What's great about these named scopes is that they do NOT use the :include option, making them <em>much</em> faster. Instead they leverage the :joins option, which is great for performance. To prove my point here is a quick benchmark from an application I am working on:
99
+ This is recursive, you can travel through your associations simply by typing it in the name of the method. Again these are just named scopes. You can chain them together, call methods off of them, etc.
100
+
101
+ Also, these conditions aren't limited to the scopes Searchlogic provides. You can use your own scopes. Like this:
102
+
103
+ LineItem.named_scope :expensive, :conditions => "line_items.price > 500"
104
+
105
+ User.orders_line_items_expensive(true)
106
+
107
+ As I stated above, Searchlogic will take care of creating the necessary joins for you. This is REALLY nice when trying to keep your code DRY, because if you wanted to use a scope like this in your User model you would have to copy over the conditions. Now you have 2 named scopes that are essentially doing the same thing. Why do that when you can dynamically access that scope using this feature?
108
+
109
+ Another thing to note is that the joins created by Searchlogic do NOT use the :include option, making them <em>much</em> faster. Instead they leverage the :joins option, which is great for performance. To prove my point here is a quick benchmark from an application I am working on:
87
110
 
88
111
  Benchmark.bm do |x|
89
112
  x.report { 10.times { Event.tickets_id_gt(10).all(:include => :tickets) } }
@@ -99,6 +122,45 @@ If you want to use the :include option, just specify it:
99
122
 
100
123
  Obviously, only do this if you want to actually use the included objects. Including objects into a query can be helpful with performance, especially when solving an N+1 query problem.
101
124
 
125
+ == Order your search
126
+
127
+ Just like the various conditions, Searchlogic gives you some very basic scopes for ordering your data:
128
+
129
+ User.ascend_by_id
130
+ User.descend_by_id
131
+ User.ascend_by_orders_line_items_price
132
+ # etc...
133
+
134
+ == Use any or all
135
+
136
+ Every condition you've seen in this readme also has 2 related conditions that you can use. Example:
137
+
138
+ User.username_like_any("bjohnson", "thunt") # will return any users that have either of the strings in their username
139
+ User.username_like_all("bjohnson", "thunt") # will return any users that have all of the strings in their username
140
+ User.username_like_any(["bjohnson", "thunt"]) # also accepts an array
141
+
142
+ This is great for checkbox filters, etc. Where you can pass an array right from your form to this condition.
143
+
144
+ == Combine scopes with 'OR'
145
+
146
+ In the same fashion that Searchlogic provides a tool for accessing scopes in associated classes, it also provides a tool for combining scopes with 'OR'. As we all know, when scopes are combined they are joined with 'AND', but sometimes you need to combine scopes with 'OR'. Searchlogic solves this problem:
147
+
148
+ User.username_or_first_name_like("ben")
149
+ => "username LIKE '%ben%' OR first_name like'%ben%'"
150
+
151
+ User.id_or_age_lt_or_username_or_first_name_begins_with(10)
152
+ => "id < 10 OR age < 10 OR username LIKE 'ben%' OR first_name like'ben%'"
153
+
154
+ Notice you don't have to specify the explicit condition (like, gt, lt, begins with, etc.). You just need to eventually specify it. If you specify a column it will just use the next condition specified. So instead of:
155
+
156
+ User.username_like_or_first_name_like("ben")
157
+
158
+ You can do:
159
+
160
+ User.username_or_first_name_like("ben")
161
+
162
+ Again, these just map to named scopes. Use Searchlogic's dynamic scopes, use scopes on associations, use your own custom scopes. As long as it maps to a named scope it will join the conditions with 'OR'. There are no limitations.
163
+
102
164
  == Make searching and ordering data in your application trivial
103
165
 
104
166
  The above is great, but what about tying all of this in with a search form in your application? What would be really nice is if we could use an object that represented a single search. Like this...
@@ -178,16 +240,6 @@ Now just throw it in your form:
178
240
 
179
241
  This really allows Searchlogic to extend beyond what it provides internally. If Searchlogic doesn't provide a named scope for that crazy edge case that you need, just create your own named scope and use it. The sky is the limit.
180
242
 
181
- == Use any or all
182
-
183
- Every condition you've seen in this readme also has 2 related conditions that you can use. Example:
184
-
185
- User.username_like_any("bjohnson", "thunt") # will return any users that have either of the strings in their username
186
- User.username_like_all("bjohnson", "thunt") # will return any users that have all of the strings in their username
187
- User.username_like_any(["bjohnson", "thunt"]) # also accepts an array
188
-
189
- This is great for checkbox filters, etc. Where you can pass an array right from your form to this condition.
190
-
191
243
  == Pagination (leverage will_paginate)
192
244
 
193
245
  Instead of recreating the wheel with pagination, Searchlogic works great with will_paginate. All that Searchlogic is doing is creating named scopes, and will_paginate works great with named scopes:
data/Rakefile CHANGED
@@ -5,14 +5,15 @@ begin
5
5
  require 'jeweler'
6
6
  Jeweler::Tasks.new do |gem|
7
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."
8
+ gem.summary = "Searchlogic provides tools that make using ActiveRecord named scopes easier and less repetitive."
9
+ gem.description = "Searchlogic provides tools that make using ActiveRecord named scopes easier and less repetitive."
10
10
  gem.email = "bjohnson@binarylogic.com"
11
11
  gem.homepage = "http://github.com/binarylogic/searchlogic"
12
12
  gem.authors = ["Ben Johnson of Binary Logic"]
13
13
  gem.rubyforge_project = "searchlogic"
14
14
  gem.add_dependency "activerecord", ">= 2.0.0"
15
15
  end
16
+ Jeweler::RubyforgeTasks.new
16
17
  rescue LoadError
17
18
  puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
19
  end
@@ -29,15 +30,6 @@ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
30
  spec.rcov = true
30
31
  end
31
32
 
33
+ task :spec => :check_dependencies
32
34
 
33
35
  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 CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 3
2
+ :minor: 3
3
+ :patch: 0
3
4
  :major: 2
4
- :minor: 2
data/lib/searchlogic.rb CHANGED
@@ -7,6 +7,7 @@ require "searchlogic/named_scopes/ordering"
7
7
  require "searchlogic/named_scopes/association_conditions"
8
8
  require "searchlogic/named_scopes/association_ordering"
9
9
  require "searchlogic/named_scopes/alias_scope"
10
+ require "searchlogic/named_scopes/or_conditions"
10
11
  require "searchlogic/search"
11
12
 
12
13
  Proc.send(:include, Searchlogic::CoreExt::Proc)
@@ -14,29 +15,23 @@ Object.send(:include, Searchlogic::CoreExt::Object)
14
15
 
15
16
  module ActiveRecord # :nodoc: all
16
17
  class Base
17
- class << self
18
- include Searchlogic::ActiveRecord::Consistency
19
- end
18
+ class << self; include Searchlogic::ActiveRecord::Consistency; end
20
19
  end
21
20
  end
22
21
 
23
22
  ActiveRecord::Base.extend(Searchlogic::ActiveRecord::NamedScopes)
24
-
25
23
  ActiveRecord::Base.extend(Searchlogic::NamedScopes::Conditions)
26
24
  ActiveRecord::Base.extend(Searchlogic::NamedScopes::AssociationConditions)
27
25
  ActiveRecord::Base.extend(Searchlogic::NamedScopes::AssociationOrdering)
28
26
  ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering)
29
27
  ActiveRecord::Base.extend(Searchlogic::NamedScopes::AliasScope)
28
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::OrConditions)
30
29
  ActiveRecord::Base.extend(Searchlogic::Search::Implementation)
31
30
 
32
31
  # Try to use the search method, if it's available. Thinking sphinx and other plugins
33
32
  # like to use that method as well.
34
33
  if !ActiveRecord::Base.respond_to?(:search)
35
- ActiveRecord::Base.class_eval do
36
- class << self
37
- alias_method :search, :searchlogic
38
- end
39
- end
34
+ ActiveRecord::Base.class_eval { class << self; alias_method :search, :searchlogic; end }
40
35
  end
41
36
 
42
37
  if defined?(ActionController)
@@ -8,7 +8,7 @@ module Searchlogic
8
8
  alias_method_chain :merge_joins, :searchlogic
9
9
  end
10
10
  end
11
-
11
+
12
12
  # In AR multiple joins are sometimes in a single join query, and other times they
13
13
  # are not. The merge_joins method in AR should account for this, but it doesn't.
14
14
  # This fixes that problem. This way there is one join per string, which allows
@@ -43,9 +43,18 @@ module Searchlogic
43
43
  # you to use when writing your own named scopes. This way you know for sure
44
44
  # that duplicate joins will be removed when chaining scopes together that
45
45
  # use the same join.
46
+ #
47
+ # Also, don't worry about breaking up the joins or retriving multiple joins.
48
+ # ActiveRecord will remove dupilicate joins and Searchlogic assists ActiveRecord in
49
+ # breaking up your joins so that they are unique.
46
50
  def inner_joins(association_name)
47
51
  ::ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
48
52
  end
53
+
54
+ # See inner_joins, except this creates LEFT OUTER joins.
55
+ def left_outer_joins(association_name)
56
+ ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
57
+ end
49
58
  end
50
59
  end
51
60
  end
@@ -1,20 +1,11 @@
1
1
  module Searchlogic
2
2
  module NamedScopes
3
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.
4
+ # scope or create a named scope procedure. See the alias_scope method for a more
5
+ # detailed explanation.
6
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
7
+ # In some instances you might create a class method that essentially aliases a named scope
8
+ # or represents a named scope procedure. Ex:
18
9
  #
19
10
  # class User
20
11
  # def teenager
@@ -22,13 +13,24 @@ module Searchlogic
22
13
  # end
23
14
  # end
24
15
  #
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:
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:
28
29
  #
29
30
  # User.alias_scope :teenager, lambda { age_gte(13).age_lte(19) }
30
31
  #
31
- # It fits in better, at the same time Searchlogic will know this is an acceptable named scope.
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.
32
34
  def alias_scope(name, options = nil)
33
35
  alias_scopes[name.to_sym] = options
34
36
  (class << self; self end).instance_eval do
@@ -1,21 +1,11 @@
1
1
  module Searchlogic
2
2
  module NamedScopes
3
- # Handles dynamically creating named scopes for associations.
3
+ # Handles dynamically creating named scopes for associations. See the README for a detailed explanation.
4
4
  module AssociationConditions
5
5
  def condition?(name) # :nodoc:
6
6
  super || association_condition?(name)
7
7
  end
8
8
 
9
- def primary_condition_name(name) # :nodoc:
10
- if result = super
11
- result
12
- elsif association_condition?(name)
13
- name.to_sym
14
- else
15
- nil
16
- end
17
- end
18
-
19
9
  private
20
10
  def association_condition?(name)
21
11
  !association_condition_details(name).nil?
@@ -31,7 +21,7 @@ module Searchlogic
31
21
  end
32
22
 
33
23
  def association_condition_details(name)
34
- assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }
24
+ assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
35
25
  return nil if assocs.empty?
36
26
 
37
27
  if name.to_s =~ /^(#{assocs.collect(&:name).join("|")})_(\w+)$/
@@ -64,23 +54,7 @@ module Searchlogic
64
54
  prepare_named_scope_options(options, association)
65
55
  options
66
56
  else
67
- # The underlying condition requires parameters, let's match the parameters it requires
68
- # and pass those onto the named scope. We can't use proxy_options because that returns the
69
- # result after a value has been passed.
70
- proc_args = []
71
- if arity > 0
72
- arity.times { |i| proc_args << "arg#{i}"}
73
- else
74
- positive_arity = arity * -1
75
- positive_arity.times do |i|
76
- if i == (positive_arity - 1)
77
- proc_args << "*arg#{i}"
78
- else
79
- proc_args << "arg#{i}"
80
- end
81
- end
82
- end
83
-
57
+ proc_args = arity_args(arity)
84
58
  arg_type = (scope_options.respond_to?(:searchlogic_arg_type) && scope_options.searchlogic_arg_type) || :string
85
59
 
86
60
  eval <<-"end_eval"
@@ -94,8 +68,27 @@ module Searchlogic
94
68
  end
95
69
  end
96
70
 
71
+ # Used to match the new scopes parameters to the underlying scope. This way we can disguise the
72
+ # new scope as best as possible instead of taking the easy way out and using *args.
73
+ def arity_args(arity)
74
+ args = []
75
+ if arity > 0
76
+ arity.times { |i| args << "arg#{i}" }
77
+ else
78
+ positive_arity = arity * -1
79
+ positive_arity.times do |i|
80
+ if i == (positive_arity - 1)
81
+ args << "*arg#{i}"
82
+ else
83
+ args << "arg#{i}"
84
+ end
85
+ end
86
+ end
87
+ args
88
+ end
89
+
97
90
  def prepare_named_scope_options(options, association)
98
- options.delete(:readonly)
91
+ options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
99
92
 
100
93
  if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
101
94
  options[:joins] = [inner_joins(association.name), options[:joins]].flatten
@@ -1,21 +1,19 @@
1
1
  module Searchlogic
2
2
  module NamedScopes
3
- # Handles dynamically creating named scopes for associations.
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.
4
12
  module AssociationOrdering
5
13
  def condition?(name) # :nodoc:
6
14
  super || association_ordering_condition?(name)
7
15
  end
8
16
 
9
- def primary_condition_name(name) # :nodoc
10
- if result = super
11
- result
12
- elsif association_ordering_condition?(name)
13
- name.to_sym
14
- else
15
- nil
16
- end
17
- end
18
-
19
17
  private
20
18
  def association_ordering_condition?(name)
21
19
  !association_ordering_condition_details(name).nil?
@@ -1,6 +1,13 @@
1
1
  module Searchlogic
2
2
  module NamedScopes
3
- # Handles dynamically creating named scopes for columns.
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.
4
11
  module Conditions
5
12
  COMPARISON_CONDITIONS = {
6
13
  :equals => [:is, :eq],
@@ -40,31 +47,6 @@ module Searchlogic
40
47
  PRIMARY_CONDITIONS = CONDITIONS.keys
41
48
  ALIAS_CONDITIONS = CONDITIONS.values.flatten
42
49
 
43
- # Returns the primary condition for the given alias. Ex:
44
- #
45
- # primary_condition(:gt) => :greater_than
46
- def primary_condition(alias_condition)
47
- CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
48
- end
49
-
50
- # Returns the primary name for any condition on a column. You can pass it
51
- # a primary condition, alias condition, etc, and it will return the proper
52
- # primary condition name. This helps simply logic throughout Searchlogic. Ex:
53
- #
54
- # primary_condition_name(:id_gt) => :id_greater_than
55
- # primary_condition_name(:id_greater_than) => :id_greater_than
56
- def primary_condition_name(name)
57
- if details = condition_details(name)
58
- if PRIMARY_CONDITIONS.include?(name.to_sym)
59
- name
60
- else
61
- "#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
62
- end
63
- else
64
- nil
65
- end
66
- end
67
-
68
50
  # Is the name of the method a valid condition that can be dynamically created?
69
51
  def condition?(name)
70
52
  local_condition?(name)
@@ -106,7 +88,7 @@ module Searchlogic
106
88
 
107
89
  scope_options = case condition.to_s
108
90
  when /^equals/
109
- scope_options(condition, column_type, "#{table_name}.#{column} = ?")
91
+ scope_options(condition, column_type, lambda { |a| attribute_condition("#{table_name}.#{column}", a) })
110
92
  when /^does_not_equal/
111
93
  scope_options(condition, column_type, "#{table_name}.#{column} != ?")
112
94
  when /^less_than_or_equal_to/
@@ -148,20 +130,22 @@ module Searchlogic
148
130
  when /_(any|all)$/
149
131
  searchlogic_lambda(column_type) { |*values|
150
132
  return {} if values.empty?
151
- values = values.flatten
152
133
 
153
- values_to_sub = nil
154
- if value_modifier.nil?
155
- values_to_sub = values
156
- else
157
- values_to_sub = values.collect { |value| value_with_modifier(value, value_modifier) }
158
- end
134
+ values.collect! { |value| value_with_modifier(value, value_modifier) }
159
135
 
160
136
  join = $1 == "any" ? " OR " : " AND "
161
- {:conditions => [values.collect { |value| sql }.join(join), *values_to_sub]}
137
+ scope_sql = values.collect { |value| sql.is_a?(Proc) ? sql.call(value) : sql }.join(join)
138
+
139
+ {:conditions => [scope_sql, *expand_range_bind_variables(values)]}
162
140
  }
163
141
  else
164
- searchlogic_lambda(column_type) { |value| {:conditions => [sql, value_with_modifier(value, value_modifier)]} }
142
+ searchlogic_lambda(column_type) { |*values|
143
+ values.collect! { |value| value_with_modifier(value, value_modifier) }
144
+
145
+ scope_sql = sql.is_a?(Proc) ? sql.call(*values) : sql
146
+
147
+ {:conditions => [scope_sql, *expand_range_bind_variables(values)]}
148
+ }
165
149
  end
166
150
  end
167
151
 
@@ -185,6 +169,31 @@ module Searchlogic
185
169
  send(primary_name, *args) # go back to method_missing and make sure we create the method
186
170
  (class << self; self; end).class_eval { alias_method alias_name, primary_name }
187
171
  end
172
+
173
+ # Returns the primary condition for the given alias. Ex:
174
+ #
175
+ # primary_condition(:gt) => :greater_than
176
+ def primary_condition(alias_condition)
177
+ CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
178
+ end
179
+
180
+ # Returns the primary name for any condition on a column. You can pass it
181
+ # a primary condition, alias condition, etc, and it will return the proper
182
+ # primary condition name. This helps simply logic throughout Searchlogic. Ex:
183
+ #
184
+ # primary_condition_name(:id_gt) => :id_greater_than
185
+ # primary_condition_name(:id_greater_than) => :id_greater_than
186
+ def primary_condition_name(name)
187
+ if details = condition_details(name)
188
+ if PRIMARY_CONDITIONS.include?(name.to_sym)
189
+ name
190
+ else
191
+ "#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
192
+ end
193
+ else
194
+ nil
195
+ end
196
+ end
188
197
  end
189
198
  end
190
199
  end
@@ -0,0 +1,106 @@
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
+ private
14
+ def or_condition?(name)
15
+ !or_conditions(name).nil?
16
+ end
17
+
18
+ def method_missing(name, *args, &block)
19
+ if conditions = or_conditions(name)
20
+ create_or_condition(conditions, args)
21
+ (class << self; self; end).class_eval { alias_method name, conditions.join("_or_") } if !respond_to?(name)
22
+ send(name, *args)
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ def or_conditions(name)
29
+ # First determine if we should even work on the name, we want to be as quick as possible
30
+ # with this.
31
+ if (parts = split_or_condition(name)).size > 1
32
+ conditions = interpolate_or_conditions(parts)
33
+ if conditions.any?
34
+ conditions
35
+ else
36
+ nil
37
+ end
38
+ end
39
+ end
40
+
41
+ def split_or_condition(name)
42
+ parts = name.to_s.split("_or_")
43
+ new_parts = []
44
+ parts.each do |part|
45
+ if part =~ /^equal_to(_any|_all)?$/
46
+ new_parts << new_parts.pop + "_or_equal_to"
47
+ else
48
+ new_parts << part
49
+ end
50
+ end
51
+ new_parts
52
+ end
53
+
54
+ # The purpose of this method is to convert the method name parts into actual condition names.
55
+ #
56
+ # Example:
57
+ #
58
+ # ["first_name", "last_name_like"]
59
+ # => ["first_name_like", "last_name_like"]
60
+ #
61
+ # ["id_gt", "first_name_begins_with", "last_name", "middle_name_like"]
62
+ # => ["id_gt", "first_name_begins_with", "last_name_like", "middle_name_like"]
63
+ #
64
+ # Basically if a column is specified without a condition the next condition in the list
65
+ # is what will be used. Once we are able to get a consistent list of conditions we can easily
66
+ # create a scope for it.
67
+ def interpolate_or_conditions(parts)
68
+ conditions = []
69
+ last_condition = nil
70
+
71
+ parts.reverse.each do |part|
72
+ if details = condition_details(part)
73
+ # We are a searchlogic defined scope
74
+ conditions << "#{details[:column]}_#{details[:condition]}"
75
+ last_condition = details[:condition]
76
+ elsif details = association_condition_details(part)
77
+ # pending, need to find the last condition
78
+ elsif local_condition?(part)
79
+ # We are a custom scope
80
+ conditions << part
81
+ elsif column_names.include?(part)
82
+ # we are a column, use the last condition
83
+ if last_condition.nil?
84
+ raise NoConditionSpecifiedError.new("The '#{part}' column doesn't know which condition to use, if you use an exact column " +
85
+ "name you need to specify a condition sometime after (ex: id_or_created_at_lt), where id would use the 'lt' condition.")
86
+ end
87
+
88
+ conditions << "#{part}_#{last_condition}"
89
+ else
90
+ raise UnknownConditionError.new("The condition '#{part}' is not a valid condition, we could not find any scopes that match this.")
91
+ end
92
+ end
93
+
94
+ conditions
95
+ end
96
+
97
+ def create_or_condition(scopes, args)
98
+ named_scope scopes.join("_or_"), lambda { |*args|
99
+ scopes_options = scopes.collect { |scope| send(scope, *args).proxy_options }
100
+ conditions = scopes_options.reject { |o| o[:conditions].nil? }.collect { |o| sanitize_sql(o[:conditions]) }
101
+ scopes.inject(scoped({})) { |scope, scope_name| scope.send(scope_name, *args) }.scope(:find).merge(:conditions => "(" + conditions.join(") OR (") + ")")
102
+ }
103
+ end
104
+ end
105
+ end
106
+ end
@@ -1,21 +1,16 @@
1
1
  module Searchlogic
2
2
  module NamedScopes
3
- # Handles dynamically creating named scopes for orderin by columns.
3
+ # Handles dynamically creating named scopes for ordering by columns. Example:
4
+ #
5
+ # User.ascend_by_id
6
+ # User.descend_by_username
7
+ #
8
+ # See the README for a more detailed explanation.
4
9
  module Ordering
5
10
  def condition?(name) # :nodoc:
6
11
  super || ordering_condition?(name)
7
12
  end
8
13
 
9
- def primary_condition_name(name) # :nodoc
10
- if result = super
11
- result
12
- elsif ordering_condition?(name)
13
- name.to_sym
14
- else
15
- nil
16
- end
17
- end
18
-
19
14
  private
20
15
  def ordering_condition?(name) # :nodoc:
21
16
  !ordering_condition_details(name).nil?
@@ -21,7 +21,10 @@ module Searchlogic
21
21
  # * <tt>:params_scope</tt> - the name of the params key to scope the order condition by, defaults to :search
22
22
  def order(search, options = {}, html_options = {})
23
23
  options[:params_scope] ||= :search
24
- options[:as] ||= options[:by].to_s.humanize
24
+ if !options[:as]
25
+ id = options[:by].to_s.downcase == "id"
26
+ options[:as] = id ? options[:by].to_s.upcase : options[:by].to_s.humanize
27
+ end
25
28
  options[:ascend_scope] ||= "ascend_by_#{options[:by]}"
26
29
  options[:descend_scope] ||= "descend_by_#{options[:by]}"
27
30
  ascending = search.order.to_s == options[:ascend_scope]
@@ -79,20 +79,21 @@ module Searchlogic
79
79
 
80
80
  private
81
81
  def method_missing(name, *args, &block)
82
- if name.to_s =~ /(\w+)=$/
83
- condition = $1.to_sym
84
- scope_name = normalize_scope_name($1)
82
+ condition_name = condition_name(name)
83
+ scope_name = scope_name(condition_name)
84
+
85
+ if setter?(name)
85
86
  if scope?(scope_name)
86
- conditions[condition] = type_cast(args.first, cast_type(scope_name))
87
+ conditions[condition_name] = type_cast(args.first, cast_type(scope_name))
87
88
  else
88
- raise UnknownConditionError.new(name)
89
+ raise UnknownConditionError.new(condition_name)
89
90
  end
90
- elsif scope?(normalize_scope_name(name))
91
+ elsif scope?(scope_name)
91
92
  if args.size > 0
92
- send("#{name}=", *args)
93
+ send("#{condition_name}=", *args)
93
94
  self
94
95
  else
95
- conditions[name]
96
+ conditions[condition_name]
96
97
  end
97
98
  else
98
99
  scope = conditions.inject(klass.scoped(current_scope)) do |scope, condition|
@@ -119,6 +120,19 @@ module Searchlogic
119
120
  klass.column_names.include?(scope_name.to_s) ? "#{scope_name}_equals".to_sym : scope_name.to_sym
120
121
  end
121
122
 
123
+ def setter?(name)
124
+ !(name.to_s =~ /=$/).nil?
125
+ end
126
+
127
+ def condition_name(name)
128
+ condition = name.to_s.match(/(\w+)=?$/)[1]
129
+ condition ? condition.to_sym : nil
130
+ end
131
+
132
+ def scope_name(condition_name)
133
+ condition_name && normalize_scope_name(condition_name)
134
+ end
135
+
122
136
  def scope?(scope_name)
123
137
  klass.scopes.key?(scope_name) || klass.condition?(scope_name)
124
138
  end
data/searchlogic.gemspec CHANGED
@@ -1,13 +1,16 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
1
4
  # -*- encoding: utf-8 -*-
2
5
 
3
6
  Gem::Specification.new do |s|
4
7
  s.name = %q{searchlogic}
5
- s.version = "2.2.3"
8
+ s.version = "2.3.0"
6
9
 
7
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
11
  s.authors = ["Ben Johnson of Binary Logic"]
9
- s.date = %q{2009-07-31}
10
- s.description = %q{Searchlogic provides common named scopes and object based searching for ActiveRecord.}
12
+ s.date = %q{2009-08-22}
13
+ s.description = %q{Searchlogic provides tools that make using ActiveRecord named scopes easier and less repetitive.}
11
14
  s.email = %q{bjohnson@binarylogic.com}
12
15
  s.extra_rdoc_files = [
13
16
  "LICENSE",
@@ -30,6 +33,7 @@ Gem::Specification.new do |s|
30
33
  "lib/searchlogic/named_scopes/association_conditions.rb",
31
34
  "lib/searchlogic/named_scopes/association_ordering.rb",
32
35
  "lib/searchlogic/named_scopes/conditions.rb",
36
+ "lib/searchlogic/named_scopes/or_conditions.rb",
33
37
  "lib/searchlogic/named_scopes/ordering.rb",
34
38
  "lib/searchlogic/rails_helpers.rb",
35
39
  "lib/searchlogic/search.rb",
@@ -41,6 +45,7 @@ Gem::Specification.new do |s|
41
45
  "spec/named_scopes/association_conditions_spec.rb",
42
46
  "spec/named_scopes/association_ordering_spec.rb",
43
47
  "spec/named_scopes/conditions_spec.rb",
48
+ "spec/named_scopes/or_conditions_spec.rb",
44
49
  "spec/named_scopes/ordering_spec.rb",
45
50
  "spec/search_spec.rb",
46
51
  "spec/spec_helper.rb"
@@ -50,7 +55,7 @@ Gem::Specification.new do |s|
50
55
  s.require_paths = ["lib"]
51
56
  s.rubyforge_project = %q{searchlogic}
52
57
  s.rubygems_version = %q{1.3.5}
53
- s.summary = %q{Searchlogic provides common named scopes and object based searching for ActiveRecord.}
58
+ s.summary = %q{Searchlogic provides tools that make using ActiveRecord named scopes easier and less repetitive.}
54
59
  s.test_files = [
55
60
  "spec/core_ext/object_spec.rb",
56
61
  "spec/core_ext/proc_spec.rb",
@@ -58,6 +63,7 @@ Gem::Specification.new do |s|
58
63
  "spec/named_scopes/association_conditions_spec.rb",
59
64
  "spec/named_scopes/association_ordering_spec.rb",
60
65
  "spec/named_scopes/conditions_spec.rb",
66
+ "spec/named_scopes/or_conditions_spec.rb",
61
67
  "spec/named_scopes/ordering_spec.rb",
62
68
  "spec/search_spec.rb",
63
69
  "spec/spec_helper.rb"
@@ -15,6 +15,7 @@ describe "Association Conditions" do
15
15
  end
16
16
 
17
17
  it "should allow the use of deep foreign pre-existing named scopes" do
18
+ pending
18
19
  Order.named_scope :big_id, :conditions => "orders.id > 100"
19
20
  Company.users_orders_big_id.proxy_options.should == Order.big_id.proxy_options.merge(:joins => {:users => :orders})
20
21
  end
@@ -15,6 +15,9 @@ describe "Conditions" do
15
15
  it "should have equals" do
16
16
  (5..7).each { |age| User.create(:age => age) }
17
17
  User.age_equals(6).all.should == User.find_all_by_age(6)
18
+ User.age_equals(nil).all.should == User.find_all_by_age(nil)
19
+ User.age_equals(5..6).all.should == User.find_all_by_age(5..6)
20
+ User.age_equals([5, 7]).all.should == User.find_all_by_age([5, 7])
18
21
  end
19
22
 
20
23
  it "should have does not equal" do
@@ -0,0 +1,24 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe "Or conditions" do
4
+ it "should match username or name" do
5
+ User.username_or_name_like("ben").proxy_options.should == {:conditions => "(users.name LIKE '%ben%') OR (users.username LIKE '%ben%')"}
6
+ end
7
+
8
+ it "should use the specified condition" do
9
+ User.username_begins_with_or_name_like("ben").proxy_options.should == {:conditions => "(users.name LIKE '%ben%') OR (users.username LIKE 'ben%')"}
10
+ end
11
+
12
+ it "should use the last specified condition" do
13
+ User.username_or_name_like_or_id_or_age_lt(10).proxy_options.should == {:conditions => "(users.age < 10) OR (users.id < 10) OR (users.name LIKE '%10%') OR (users.username LIKE '%10%')"}
14
+ end
15
+
16
+ it "should raise an error on unknown conditions" do
17
+ lambda { User.usernme_begins_with_or_name_like("ben") }.should raise_error(Searchlogic::NamedScopes::OrConditions::UnknownConditionError)
18
+ end
19
+
20
+ it "should play nice with other scopes" do
21
+ User.username_begins_with("ben").id_gt(10).age_not_nil.username_or_name_ends_with("ben").scope(:find).should ==
22
+ {:conditions => "((users.name LIKE '%ben') OR (users.username LIKE '%ben')) AND ((users.age IS NOT NULL) AND ((users.id > 10) AND (users.username LIKE 'ben%')))"}
23
+ end
24
+ end
data/spec/spec_helper.rb CHANGED
@@ -71,6 +71,7 @@ Spec::Runner.configure do |config|
71
71
  class User < ActiveRecord::Base
72
72
  belongs_to :company, :counter_cache => true
73
73
  has_many :orders, :dependent => :destroy
74
+ has_many :orders_big, :class_name => 'Order', :conditions => 'total > 100'
74
75
  end
75
76
 
76
77
  class Order < ActiveRecord::Base
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchlogic
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.3
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Johnson of Binary Logic
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-07-31 00:00:00 -04:00
12
+ date: 2009-08-22 00:00:00 -04:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -22,7 +22,7 @@ dependencies:
22
22
  - !ruby/object:Gem::Version
23
23
  version: 2.0.0
24
24
  version:
25
- description: Searchlogic provides common named scopes and object based searching for ActiveRecord.
25
+ description: Searchlogic provides tools that make using ActiveRecord named scopes easier and less repetitive.
26
26
  email: bjohnson@binarylogic.com
27
27
  executables: []
28
28
 
@@ -48,6 +48,7 @@ files:
48
48
  - lib/searchlogic/named_scopes/association_conditions.rb
49
49
  - lib/searchlogic/named_scopes/association_ordering.rb
50
50
  - lib/searchlogic/named_scopes/conditions.rb
51
+ - lib/searchlogic/named_scopes/or_conditions.rb
51
52
  - lib/searchlogic/named_scopes/ordering.rb
52
53
  - lib/searchlogic/rails_helpers.rb
53
54
  - lib/searchlogic/search.rb
@@ -59,6 +60,7 @@ files:
59
60
  - spec/named_scopes/association_conditions_spec.rb
60
61
  - spec/named_scopes/association_ordering_spec.rb
61
62
  - spec/named_scopes/conditions_spec.rb
63
+ - spec/named_scopes/or_conditions_spec.rb
62
64
  - spec/named_scopes/ordering_spec.rb
63
65
  - spec/search_spec.rb
64
66
  - spec/spec_helper.rb
@@ -89,7 +91,7 @@ rubyforge_project: searchlogic
89
91
  rubygems_version: 1.3.5
90
92
  signing_key:
91
93
  specification_version: 3
92
- summary: Searchlogic provides common named scopes and object based searching for ActiveRecord.
94
+ summary: Searchlogic provides tools that make using ActiveRecord named scopes easier and less repetitive.
93
95
  test_files:
94
96
  - spec/core_ext/object_spec.rb
95
97
  - spec/core_ext/proc_spec.rb
@@ -97,6 +99,7 @@ test_files:
97
99
  - spec/named_scopes/association_conditions_spec.rb
98
100
  - spec/named_scopes/association_ordering_spec.rb
99
101
  - spec/named_scopes/conditions_spec.rb
102
+ - spec/named_scopes/or_conditions_spec.rb
100
103
  - spec/named_scopes/ordering_spec.rb
101
104
  - spec/search_spec.rb
102
105
  - spec/spec_helper.rb