kazjote-searchlogic 2.1.9.3 → 2.3.4

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,4 +1,61 @@
1
- == 2.1.8
1
+ == 2.3.3 released 2009-09-02
2
+
3
+ * Split out merging scopes with 'or' into a convenient method.
4
+
5
+ == 2.3.2 released 2009-08-26
6
+
7
+ * Add in scope_procedure as an alias for alias_scope.
8
+ * Fixed bug with not_blank condition.
9
+
10
+ == 2.3.1 released 2009-08-24
11
+
12
+ * Added blank and not_blank conditions.
13
+ * Made User.whatever_like_any("val1", "val2") consistent with User.whatever_like_any(["val1", "val2"])
14
+
15
+ == 2.3.0 released 2009-08-22
16
+
17
+ * Thanks to laserlemon for support of ranges and arrays in the equals condition.
18
+ * Added feature to combine condition with 'or'.
19
+
20
+ == 2.2.3 released 2009-07-31
21
+
22
+ * 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.
23
+
24
+ == 2.2.2 released 2009-07-31
25
+
26
+ * Fix bug to give priority to local columns.
27
+
28
+ == 2.2.1 released 2009-07-30
29
+
30
+ * Use ::ActiveRecord instead of ActiveRecord to avoid a name conflict since ActiveRecord is a module within Searchlogic.
31
+
32
+ == 2.2.0 released 2009-07-30
33
+
34
+ * Refactored association code to be much simpler and rely on recursion. This allows the underlying class to do most of the work. This also allows calling any named scopes through any level of associations.
35
+
36
+ == 2.1.13 released 2009-07-29
37
+
38
+ * Applied bug fix from http://github.com/skanev/searchlogic to make #order work with association ordering.
39
+ * Applied bug fix to allow for custom ordering conditions.
40
+
41
+ == 2.1.12 released 2009-07-28
42
+
43
+ * Fixed bug when dealing with scopes that return nil.
44
+
45
+ == 2.1.11 released 2009-07-28
46
+
47
+ * Reworked how alias conditions are created on the fly, uses scope(:find) instead of proxy_options to create the scope. This allows using association alias named scopes.
48
+
49
+ == 2.1.10 released 2009-07-28
50
+
51
+ * Ignore polymorphic associations when dynamically creating conditions on associations.
52
+
53
+ == 2.1.9 released 2009-07-28
54
+
55
+ * Fixed bug when cloning with no scope
56
+ * Allow the call of foreign pre-existing named scopes instead of those generated by searchlogic. Allows you to call named scopes on associations that you define yourself.
57
+
58
+ == 2.1.8 released 2009-07-15
2
59
 
3
60
  * Added support for not_like, not_begin_with, not_end_with, and not_null
4
61
 
data/README.rdoc CHANGED
@@ -1,19 +1,18 @@
1
1
  = Searchlogic
2
2
 
3
- <b>Searchlogic has been <em>completely</em> rewritten for v2. It is much simpler and has taken an entirely new approach. To give you an idea, v1 had ~2300 lines of code, v2 has ~420 lines of code.</b>
4
-
5
- Searchlogic provides common named scopes and object based searching for ActiveRecord.
3
+ Searchlogic makes using ActiveRecord named scopes easier and less repetitive. It helps keep your code DRY, clean, and simple.
6
4
 
7
5
  == Helpful links
8
6
 
9
7
  * <b>Documentation:</b> http://rdoc.info/projects/binarylogic/searchlogic
10
8
  * <b>Repository:</b> http://github.com/binarylogic/searchlogic/tree/master
11
- * <b>Bugs / feature suggestions:</b> http://binarylogic.lighthouseapp.com/projects/16601-searchlogic
9
+ * <b>Issues:</b> http://github.com/binarylogic/searchlogic/issues
12
10
  * <b>Google group:</b> http://groups.google.com/group/searchlogic
11
+ * <b>Railscast:</b> http://railscasts.com/episodes/176-searchlogic
13
12
 
14
13
  <b>Before contacting me directly, please read:</b>
15
14
 
16
- 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.
15
+ 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.
17
16
 
18
17
  == Install & use
19
18
 
@@ -42,35 +41,52 @@ Instead of explaining what Searchlogic can do, let me show you. Let's start at t
42
41
 
43
42
  # Searchlogic gives you a bunch of named scopes for free:
44
43
  User.username_equals("bjohnson")
44
+ User.username_equals(["bjohnson", "thunt"])
45
+ User.username_equals("a".."b")
45
46
  User.username_does_not_equal("bjohnson")
46
47
  User.username_begins_with("bjohnson")
48
+ User.username_not_begin_with("bjohnson")
47
49
  User.username_like("bjohnson")
50
+ User.username_not_like("bjohnson")
48
51
  User.username_ends_with("bjohnson")
52
+ User.username_not_end_with("bjohnson")
49
53
  User.age_greater_than(20)
50
54
  User.age_greater_than_or_equal_to(20)
51
55
  User.age_less_than(20)
52
56
  User.age_less_than_or_equal_to(20)
53
57
  User.username_null
58
+ User.username_not_null
54
59
  User.username_blank
55
-
56
- # You can also order by columns
57
- User.ascend_by_username
58
- User.descend_by_username
59
- User.order("ascend_by_username")
60
60
 
61
61
  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:
62
62
 
63
- scope = User.username_like("bjohnson").age_greater_than(20).ascend_by_username
63
+ scope = User.username_like("bjohnson").age_greater_than(20).id_less_than(55)
64
64
  scope.all
65
65
  scope.first
66
66
  scope.count
67
67
  # etc...
68
68
 
69
- That's all pretty standard, but here's where Searchlogic starts to get interesting...
69
+ For a complete list of conditions please see the constants in Searchlogic::NamedScopes::Conditions.
70
+
71
+ == Use condition aliases
72
+
73
+ 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:
74
+
75
+ User.username_is(10) # equals
76
+ User.username_eq(10) # equals
77
+ User.id_lt(10) # less than
78
+ User.id_lte(10) # less than or equal to
79
+ User.id_gt(10) # greater than
80
+ User.id_gte(10) # greater than or equal to
81
+ # etc...
82
+
83
+ == Search using scopes in associated classes
70
84
 
71
- == Search using conditions on associated columns
85
+ 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:
72
86
 
73
- You also get named scopes for any of your associations:
87
+ === Searchlogic provided scopes
88
+
89
+ Let's take some basic scopes that Searchlogic provides for every model:
74
90
 
75
91
  # We have the following relationships
76
92
  User.has_many :orders
@@ -85,7 +101,21 @@ You also get named scopes for any of your associations:
85
101
  User.ascend_by_order_total
86
102
  User.descend_by_orders_line_items_price
87
103
 
88
- 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 create a INNER JOIN and pass it to the :joins option, which is great for performance. To prove my point here is a quick benchmark from an application I am working on:
104
+ 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.
105
+
106
+ === Custom associated scopes
107
+
108
+ Also, these conditions aren't limited to the scopes Searchlogic provides. You can use your own scopes. Like this:
109
+
110
+ LineItem.named_scope :expensive, :conditions => "line_items.price > 500"
111
+
112
+ User.orders_line_items_expensive
113
+
114
+ 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?
115
+
116
+ === Uses :joins not :include
117
+
118
+ 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:
89
119
 
90
120
  Benchmark.bm do |x|
91
121
  x.report { 10.times { Event.tickets_id_gt(10).all(:include => :tickets) } }
@@ -99,7 +129,58 @@ If you want to use the :include option, just specify it:
99
129
 
100
130
  User.orders_line_items_price_greater_than(20).all(:include => {:orders => :line_items})
101
131
 
102
- Obviously, only do this if you want to actually use the included objects.
132
+ 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.
133
+
134
+ == Order your search
135
+
136
+ Just like the various conditions, Searchlogic gives you some very basic scopes for ordering your data:
137
+
138
+ User.ascend_by_id
139
+ User.descend_by_id
140
+ User.ascend_by_orders_line_items_price
141
+ # etc...
142
+
143
+ == Use any or all
144
+
145
+ Every condition you've seen in this readme also has 2 related conditions that you can use. Example:
146
+
147
+ User.username_like_any("bjohnson", "thunt") # will return any users that have either of the strings in their username
148
+ User.username_like_all("bjohnson", "thunt") # will return any users that have all of the strings in their username
149
+ User.username_like_any(["bjohnson", "thunt"]) # also accepts an array
150
+
151
+ This is great for checkbox filters, etc. Where you can pass an array right from your form to this condition.
152
+
153
+ == Combine scopes with 'OR'
154
+
155
+ 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:
156
+
157
+ User.username_or_first_name_like("ben")
158
+ => "username LIKE '%ben%' OR first_name like'%ben%'"
159
+
160
+ User.id_or_age_lt_or_username_or_first_name_begins_with(10)
161
+ => "id < 10 OR age < 10 OR username LIKE 'ben%' OR first_name like'ben%'"
162
+
163
+ 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:
164
+
165
+ User.username_like_or_first_name_like("ben")
166
+
167
+ You can do:
168
+
169
+ User.username_or_first_name_like("ben")
170
+
171
+ 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.
172
+
173
+ == Create scope procedures
174
+
175
+ Sometimes you notice a pattern in your application where you are constantly combining certain named scopes. You want to keep the flexibility of being able to mix and match small named scopes, while at the same time being able to call a single scope for a common task. User searchlogic's scpe procedure:
176
+
177
+ User.scope_procedure :awesome, lambda { first_name_begins_with("ben").last_name_begins_with("johnson").website_equals("binarylogic.com") }
178
+
179
+ All that this is doing is creating a class level method, but what is nice about this method is that is more inline with your other named scopes. It also tells searchlogic that this method is 'safe' to use when using the search method. Ex:
180
+
181
+ User.search(:awesome => true)
182
+
183
+ Otherwise searchlogic will ignore the 'awesome' condition because there is no way to tell that its a valid scope. This is a security measure to keep users from passing in a scope with a named like 'destroy_all'.
103
184
 
104
185
  == Make searching and ordering data in your application trivial
105
186
 
@@ -178,17 +259,7 @@ Now just throw it in your form:
178
259
  = f.check_box :four_year_olds
179
260
  = f.submit
180
261
 
181
- What's great about this is that you can do just about anything you want. If Searchlogic doesn't provide a named scope for that crazy edge case that you need, just create your own named scope. The sky is the limit.
182
-
183
- == Use any or all
184
-
185
- Every condition you've seen in this readme also has 2 related conditions that you can use. Example:
186
-
187
- User.username_like_any("bjohnson", "thunt") # will return any users that have either of the strings in their username
188
- User.username_like_all("bjohnson", "thunt") # will return any users that have all of the strings in their username
189
- User.username_like_any(["bjohnson", "thunt"]) # also accepts an array
190
-
191
- This is great for checkbox filters, etc. Where you can pass an array right from your form to this condition.
262
+ 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.
192
263
 
193
264
  == Pagination (leverage will_paginate)
194
265
 
@@ -201,7 +272,7 @@ If you don't like will_paginate, use another solution, or roll your own. Paginat
201
272
 
202
273
  == Conflicts with other gems
203
274
 
204
- You will notice searchlogic wants to create a method called "search". So do other libraries like thinking sphinx, etc. So searchlogic has a no conflict resolution. If the "search" method is already taken the method will be called "searchlogic" instead. So instead of
275
+ You will notice searchlogic wants to create a method called "search". So do other libraries like thinking-sphinx, etc. So searchlogic has a no conflict resolution. If the "search" method is already taken the method will be called "searchlogic" instead. So instead of
205
276
 
206
277
  User.search
207
278
 
@@ -215,6 +286,8 @@ Before I use a library in my application I like to glance at the source and try
215
286
 
216
287
  Searchlogic utilizes method_missing to create all of these named scopes. When it hits method_missing it creates a named scope to ensure it will never hit method missing for that named scope again. Sort of a caching mechanism. It works in the same fashion as ActiveRecord's "find_by_*" methods. This way only the named scopes you need are created and nothing more.
217
288
 
289
+ The search object is just a proxy to your model that only delegates calls that map to named scopes and nothing more. This is obviously done for security reasons. It also helps make form integration easier, by type casting values, and playing nice with form_for. This class is pretty simple as well.
290
+
218
291
  That's about it, the named scope options are pretty bare bones and created just like you would manually.
219
292
 
220
293
  == Credit
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 makes using ActiveRecord named scopes easier and less repetitive."
9
+ gem.description = "Searchlogic makes 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
3
  :major: 2
3
- :minor: 1
4
- :patch: 8
4
+ :minor: 3
@@ -0,0 +1,22 @@
1
+ module Searchlogic
2
+ module ActiveRecord
3
+ # Active Record is pretty inconsistent with how their SQL is constructed. This
4
+ # method attempts to close the gap between the various inconsistencies.
5
+ module Consistency
6
+ def self.included(klass)
7
+ klass.class_eval do
8
+ alias_method_chain :merge_joins, :searchlogic
9
+ end
10
+ end
11
+
12
+ # In AR multiple joins are sometimes in a single join query, and other times they
13
+ # are not. The merge_joins method in AR should account for this, but it doesn't.
14
+ # This fixes that problem. This way there is one join per string, which allows
15
+ # the merge_joins method to delete duplicates.
16
+ def merge_joins_with_searchlogic(*args)
17
+ joins = merge_joins_without_searchlogic(*args)
18
+ joins.collect { |j| j.is_a?(String) ? j.split(" ") : j }.flatten.uniq
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,60 @@
1
+ module Searchlogic
2
+ module ActiveRecord
3
+ # Adds methods that give extra information about a classes named scopes.
4
+ module NamedScopes
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 : primary_condition_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
+ # A convenience method for creating inner join sql to that your inner joins
42
+ # are consistent with how Active Record creates them. Basically a tool for
43
+ # you to use when writing your own named scopes. This way you know for sure
44
+ # that duplicate joins will be removed when chaining scopes together that
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.
50
+ def inner_joins(association_name)
51
+ ::ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
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
58
+ end
59
+ end
60
+ end
@@ -27,8 +27,10 @@ module Searchlogic
27
27
  #
28
28
  # named_scope :id_gt, searchlogic_lambda(:integer) { |value| {:conditions => ["id > ?", value]} }
29
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.
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.
32
34
  def searchlogic_lambda(type = :string, &block)
33
35
  proc = lambda(&block)
34
36
  proc.searchlogic_arg_type = type
@@ -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
@@ -42,12 +44,14 @@ module Searchlogic
42
44
  end
43
45
  end
44
46
  end
47
+ alias_method :scope_procedure, :alias_scope
45
48
 
46
49
  def alias_scopes # :nodoc:
47
50
  @alias_scopes ||= {}
48
51
  end
49
52
 
50
53
  def alias_scope?(name) # :nodoc:
54
+ return false if name.blank?
51
55
  alias_scopes.key?(name.to_sym)
52
56
  end
53
57
 
@@ -1,47 +1,19 @@
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
- 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 }
6
+ super || association_condition?(name)
36
7
  end
37
8
 
38
9
  private
10
+ def association_condition?(name)
11
+ !association_condition_details(name).nil?
12
+ end
13
+
39
14
  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)
15
+ if !local_condition?(name) && details = association_condition_details(name)
16
+ create_association_condition(details[:association], details[:condition], args)
45
17
  send(name, *args)
46
18
  else
47
19
  super
@@ -49,29 +21,24 @@ module Searchlogic
49
21
  end
50
22
 
51
23
  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}
24
+ assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
25
+ return nil if assocs.empty?
26
+
27
+ if name.to_s =~ /^(#{assocs.collect(&:name).join("|")})_(\w+)$/
28
+ association_name = $1
29
+ condition = $2
30
+ association = reflect_on_association(association_name.to_sym)
31
+ klass = association.klass
32
+ if klass.condition?(condition)
33
+ {:association => $1, :condition => $2}
34
+ else
35
+ nil
36
+ end
66
37
  end
67
38
  end
68
39
 
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 }
40
+ def create_association_condition(association, condition, args)
41
+ named_scope("#{association}_#{condition}", association_condition_options(association, condition, args))
75
42
  end
76
43
 
77
44
  def association_condition_options(association_name, association_condition, args)
@@ -83,35 +50,52 @@ module Searchlogic
83
50
  if !arity || arity == 0
84
51
  # The underlying condition doesn't require any parameters, so let's just create a simple
85
52
  # 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]}
53
+ options = scope.scope(:find)
54
+ prepare_named_scope_options(options, association)
88
55
  options
89
56
  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
57
+ proc_args = arity_args(arity)
58
+ arg_type = (scope_options.respond_to?(:searchlogic_arg_type) && scope_options.searchlogic_arg_type) || :string
59
+
106
60
  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]}
61
+ searchlogic_lambda(:#{arg_type}) { |#{proc_args.join(",")}|
62
+ scope = association.klass.send(association_condition, #{proc_args.join(",")})
63
+ options = scope ? scope.scope(:find) : {}
64
+ prepare_named_scope_options(options, association)
110
65
  options
111
66
  }
112
67
  end_eval
113
68
  end
114
69
  end
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
+
90
+ def prepare_named_scope_options(options, association)
91
+ options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
92
+
93
+ if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
94
+ options[:joins] = [inner_joins(association.name), options[:joins]].flatten
95
+ else
96
+ options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
97
+ end
98
+ end
115
99
  end
116
100
  end
117
101
  end