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 +58 -1
- data/README.rdoc +101 -28
- data/Rakefile +4 -12
- data/VERSION.yml +2 -2
- data/lib/searchlogic/active_record/consistency.rb +22 -0
- data/lib/searchlogic/active_record/named_scopes.rb +60 -0
- data/lib/searchlogic/core_ext/object.rb +4 -2
- data/lib/searchlogic/named_scopes/alias_scope.rb +21 -17
- data/lib/searchlogic/named_scopes/association_conditions.rb +61 -77
- data/lib/searchlogic/named_scopes/association_ordering.rb +22 -6
- data/lib/searchlogic/named_scopes/conditions.rb +74 -103
- data/lib/searchlogic/named_scopes/or_conditions.rb +116 -0
- data/lib/searchlogic/named_scopes/ordering.rb +17 -22
- data/lib/searchlogic/rails_helpers.rb +5 -1
- data/lib/searchlogic/search.rb +47 -26
- data/lib/searchlogic.rb +14 -7
- data/searchlogic.gemspec +13 -6
- data/spec/named_scopes/alias_scope_spec.rb +4 -0
- data/spec/named_scopes/association_conditions_spec.rb +32 -2
- data/spec/named_scopes/association_ordering_spec.rb +8 -0
- data/spec/named_scopes/conditions_spec.rb +21 -3
- data/spec/named_scopes/or_conditions_spec.rb +36 -0
- data/spec/named_scopes/ordering_spec.rb +7 -0
- data/spec/search_spec.rb +35 -1
- data/spec/spec_helper.rb +31 -9
- metadata +10 -7
- data/lib/searchlogic/active_record_consistency.rb +0 -27
data/CHANGELOG.rdoc
CHANGED
@@ -1,4 +1,61 @@
|
|
1
|
-
== 2.
|
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
|
-
|
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>
|
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
|
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).
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
9
|
-
gem.description = "Searchlogic
|
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
@@ -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
|
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
|
5
|
-
#
|
4
|
+
# scope or create a named scope procedure. See the alias_scope method for a more
|
5
|
+
# detailed explanation.
|
6
6
|
module AliasScope
|
7
|
-
#
|
8
|
-
#
|
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
|
-
#
|
26
|
-
#
|
27
|
-
#
|
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
|
-
#
|
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)
|
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[:
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
70
|
-
|
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.
|
87
|
-
options
|
53
|
+
options = scope.scope(:find)
|
54
|
+
prepare_named_scope_options(options, association)
|
88
55
|
options
|
89
56
|
else
|
90
|
-
|
91
|
-
|
92
|
-
|
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(:#{
|
108
|
-
|
109
|
-
options
|
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
|