searchgasm 1.1.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,4 +1,13 @@
1
- == 1.1.3 released 2008-09-22
1
+ == 1.2.0 released 2008-09-24
2
+
3
+ * Added searchgasm_params and searchgasm_url helper to use outside of the control type helpers.
4
+ * Added dup and clone methods that work properly for searchgasm objects
5
+ * Fixed bug to remove nil scope values, HABTM likes to add :limit => nil
6
+ * Removed unnecessary build_search methods for associations
7
+ * Removed support for searching with conditions only. This just made things much more complicated when you can accomplish the same thing by starting a new search and only setting conditions.
8
+ * Fixed bug when searching with *any* conditions to use left outer joins instead of inner joins.
9
+
10
+ == 1.1.3 released 2008-09-23
2
11
 
3
12
  * Setting a condition to nil removes it if the condition is set to ignore blanks
4
13
  * Setting search.conditions = "some sql" will reset ALL conditions. Alternatively search.conditions => {:first_name_contains => "Ben"} will overwrite "some sql". The same goes with search.conditions.first_name_contains = "Ben".
data/Manifest CHANGED
@@ -38,7 +38,7 @@ lib/searchgasm/search/conditions.rb
38
38
  lib/searchgasm/search/ordering.rb
39
39
  lib/searchgasm/search/pagination.rb
40
40
  lib/searchgasm/search/protection.rb
41
- lib/searchgasm/shared/searching.rb
41
+ lib/searchgasm/search/searching.rb
42
42
  lib/searchgasm/shared/utilities.rb
43
43
  lib/searchgasm/shared/virtual_classes.rb
44
44
  lib/searchgasm/version.rb
@@ -49,6 +49,7 @@ Rakefile
49
49
  README.rdoc
50
50
  test/fixtures/accounts.yml
51
51
  test/fixtures/orders.yml
52
+ test/fixtures/user_groups.yml
52
53
  test/fixtures/users.yml
53
54
  test/libs/acts_as_tree.rb
54
55
  test/libs/rexml_fix.rb
@@ -65,4 +66,3 @@ test/test_search_conditions.rb
65
66
  test/test_search_ordering.rb
66
67
  test/test_search_pagination.rb
67
68
  test/test_search_protection.rb
68
- test/text_config.rb
data/README.rdoc CHANGED
@@ -177,19 +177,6 @@ Any of the options used in the above example can be used in these, but for the s
177
177
  search.conditions.first_name_contains = "Ben"
178
178
  search.per_page = 20
179
179
  search.all
180
-
181
- == Search with conditions only
182
-
183
- Don't need pagination, ordering, or any of the other options? Search with conditions only.
184
-
185
- conditions = User.new_conditions(:age_gt => 18)
186
- conditions.first_name_contains = "Ben"
187
- conditions.all
188
- # ... all operations above are available
189
-
190
- Pass a conditions object right into ActiveRecord:
191
-
192
- User.all(:conditions => conditions)
193
180
 
194
181
  == Match ANY or ALL of the conditions
195
182
 
@@ -250,12 +237,11 @@ or
250
237
 
251
238
  == Always use protection...against SQL injections
252
239
 
253
- If there is one thing we all know, it's to always use protection against SQL injections. That's why searchgasm protects you by default. The new\_search and new\_conditions methods protect mass assignments by default (instantiation and search.options = {}). This means that various checks are done to ensure it is not possible to perform any type of SQL injection during mass assignments. But this also limits how you can search, meaning you can't write raw SQL. If you want to be daring and search without protection, all that you have to do is add ! to the end of the methods: new\_search! and new\_conditions!.
240
+ If there is one thing we all know, it's to always use protection against SQL injections. That's why searchgasm protects you by default. The new\_search methods protect mass assignments by default (instantiation and search.options = {}). This means that various checks are done to ensure it is not possible to perform any type of SQL injection during mass assignments. But this also limits how you can search, meaning you can't write raw SQL. If you want to be daring and search without protection, all that you have to do is add ! to the end of the method: new\_search!.
254
241
 
255
242
  === Protected from SQL injections
256
243
 
257
244
  search = Account.new_search(params[:search])
258
- conditions = Account.new_conditions(params[:conditions])
259
245
 
260
246
  === *NOT* protected from SQL injections
261
247
 
@@ -263,11 +249,10 @@ If there is one thing we all know, it's to always use protection against SQL inj
263
249
  accounts = Account.all(params[:search])
264
250
  account = Account.first(params[:search])
265
251
  search = Account.new_search!(params[:search])
266
- conditions = Account.new_conditions!(params[:conditions])
267
252
 
268
253
  I'm sure you already knew this, but it's tempting to do this when you can pass the params hash right into these methods.
269
254
 
270
- Lesson learned: use new\_search and new\_conditions when passing in params as *ANY* of the options.
255
+ Lesson learned: use new\_search when passing in params as *ANY* of the options.
271
256
 
272
257
  == Available Conditions
273
258
 
data/Rakefile CHANGED
@@ -10,7 +10,7 @@ Echoe.new 'searchgasm' do |p|
10
10
  p.project = 'searchgasm'
11
11
  p.summary = "Object based ActiveRecord searching, ordering, pagination, and more!"
12
12
  p.url = "http://github.com/binarylogic/searchgasm"
13
- p.dependencies = %w(activerecord activesupport)
13
+ p.dependencies = ['activerecord', 'activesupport >= 2.1.0']
14
14
  p.include_rakefile = true
15
15
  end
16
16
 
@@ -11,26 +11,6 @@ module Searchgasm
11
11
  args << filter_options_with_searchgasm(options)
12
12
  find_without_searchgasm(*args)
13
13
  end
14
-
15
- # See build_conditions under Searchgasm::ActiveRecord::Base. This is the same thing but for associations.
16
- def build_conditions(options = {}, &block)
17
- @reflection.klass.send(:with_scope, :find => construct_scope[:find]) { @reflection.klass.build_conditions(options, &block) }
18
- end
19
-
20
- # See build_conditions! under Searchgasm::ActiveRecord::Base. This is the same thing but for associations.
21
- def build_conditions!(options = {}, &block)
22
- @reflection.klass.send(:with_scope, :find => construct_scope[:find]) { @reflection.klass.build_conditions!(options, &block) }
23
- end
24
-
25
- # See build_search under Searchgasm::ActiveRecord::Base. This is the same thing but for associations.
26
- def build_search(options = {}, &block)
27
- @reflection.klass.send(:with_scope, :find => construct_scope[:find]) { @reflection.klass.build_search(options, &block) }
28
- end
29
-
30
- # See build_conditions! under Searchgasm::ActiveRecord::Base. This is the same thing but for associations.
31
- def build_search!(options = {}, &block)
32
- @reflection.klass.send(:with_scope, :find => construct_scope[:find]) { @reflection.klass.build_search!(options, &block) }
33
- end
34
14
  end
35
15
 
36
16
  module Shared
@@ -11,6 +11,7 @@ module Searchgasm
11
11
  def calculate_with_searchgasm(*args)
12
12
  options = args.extract_options!
13
13
  options = filter_options_with_searchgasm(options, false)
14
+ args[1] = primary_key if options[:distinct] && [nil, :all].include?(args[1]) # quick fix for adding a column name if distinct is true and no specific column is provided
14
15
  args << options
15
16
  calculate_without_searchgasm(*args)
16
17
  end
@@ -47,30 +48,6 @@ module Searchgasm
47
48
  with_scope_without_searchgasm(method_scoping, action, &block)
48
49
  end
49
50
 
50
- # This is a special method that Searchgasm adds in. It returns a new conditions object on the model. So you can search by conditions *only*.
51
- #
52
- # <b>This method is "protected". Meaning it checks the passed options for SQL injections. So trying to write raw SQL in *any* of the option will result in a raised exception. It's safe to pass a params object when instantiating.</b>
53
- #
54
- # === Examples
55
- #
56
- # conditions = User.new_conditions
57
- # conditions.first_name_contains = "Ben"
58
- # conditions.all # can call any search method: first, find(:all), find(:first), sum("id"), etc...
59
- def build_conditions(values = {}, &block)
60
- conditions = searchgasm_conditions
61
- conditions.protect = true
62
- conditions.conditions = values
63
- yield conditions if block_given?
64
- conditions
65
- end
66
-
67
- # See build_conditions. This is the same method but *without* protection. Do *NOT* pass in a params object to this method.
68
- def build_conditions!(values = {}, &block)
69
- conditions = searchgasm_conditions(values)
70
- yield conditions if block_given?
71
- conditions
72
- end
73
-
74
51
  # This is a special method that Searchgasm adds in. It returns a new search object on the model. So you can search via an object.
75
52
  #
76
53
  # <b>This method is "protected". Meaning it checks the passed options for SQL injections. So trying to write raw SQL in *any* of the option will result in a raised exception. It's safe to pass a params object when instantiating.</b>
@@ -86,7 +63,7 @@ module Searchgasm
86
63
  # search.order_by = {:user_group => :name}
87
64
  # search.all # can call any search method: first, find(:all), find(:first), sum("id"), etc...
88
65
  def build_search(options = {}, &block)
89
- search = searchgasm_searcher
66
+ search = searchgasm_search
90
67
  search.protect = true
91
68
  search.options = options
92
69
  yield search if block_given?
@@ -97,7 +74,7 @@ module Searchgasm
97
74
  #
98
75
  # This also has an alias "new_search!"
99
76
  def build_search!(options = {}, &block)
100
- search = searchgasm_searcher(options)
77
+ search = searchgasm_search(options)
101
78
  yield search if block_given?
102
79
  search
103
80
  end
@@ -120,7 +97,7 @@ module Searchgasm
120
97
 
121
98
  # This is the reverse of conditions_protected. You can specify conditions here and *only* these conditions will be allowed in mass assignment. Any condition not specified here will be blocked.
122
99
  def conditions_accessible(*conditions)
123
- write_inheritable_attribute(:conditions_accessible, Set.new(conditions.map(&:to_s)) + (protected_conditions || []))
100
+ write_inheritable_attribute(:conditions_accessible, Set.new(conditions.map(&:to_s)) + (accessible_conditions || []))
124
101
  end
125
102
 
126
103
  def accessible_conditions # :nodoc:
@@ -130,7 +107,7 @@ module Searchgasm
130
107
  private
131
108
  def filter_options_with_searchgasm(options = {}, searching = true)
132
109
  return options unless Searchgasm::Search::Base.needed?(self, options)
133
- search = Searchgasm::Search::Base.create_virtual_class(self).new # call explicitly to avoid merging the scopes into the searcher
110
+ search = Searchgasm::Search::Base.create_virtual_class(self).new # call explicitly to avoid merging the scopes into the search
134
111
  search.acting_as_filter = true
135
112
  conditions = options.delete(:conditions) || options.delete("conditions") || {}
136
113
  if conditions
@@ -145,15 +122,7 @@ module Searchgasm
145
122
  search.sanitize(searching)
146
123
  end
147
124
 
148
- def searchgasm_conditions(options = {})
149
- searcher = Searchgasm::Conditions::Base.create_virtual_class(self).new
150
- conditions = scope(:find) && scope(:find)[:conditions]
151
- searcher.scope = {:conditions => conditions} if conditions
152
- searcher.conditions = options
153
- searcher
154
- end
155
-
156
- def searchgasm_searcher(options = {})
125
+ def searchgasm_search(options = {})
157
126
  scope = {}
158
127
  current_scope = scope(:find) && scope(:find).deep_dup
159
128
  if current_scope
@@ -162,12 +131,17 @@ module Searchgasm
162
131
  next if value.blank?
163
132
  scope[option] = value
164
133
  end
134
+
135
+ # Delete nil values in the scope, for some reason habtm relationships like to pass :limit => nil
136
+ new_scope = {}
137
+ current_scope.each { |k, v| new_scope[k] = v unless v.nil? }
138
+ current_scope = new_scope
165
139
  end
166
- searcher = Searchgasm::Search::Base.create_virtual_class(self).new
167
- searcher.scope = scope
168
- searcher.options = current_scope
169
- searcher.options = options
170
- searcher
140
+ search = Searchgasm::Search::Base.create_virtual_class(self).new
141
+ search.scope = scope
142
+ search.options = current_scope
143
+ search.options = options
144
+ search
171
145
  end
172
146
  end
173
147
  end
@@ -181,8 +155,6 @@ module ActiveRecord #:nodoc: all
181
155
  alias_method_chain :calculate, :searchgasm
182
156
  alias_method_chain :find, :searchgasm
183
157
  alias_method_chain :with_scope, :searchgasm
184
- alias_method :new_conditions, :build_conditions
185
- alias_method :new_conditions!, :build_conditions!
186
158
  alias_method :new_search, :build_search
187
159
  alias_method :new_search!, :build_search!
188
160
 
@@ -6,7 +6,6 @@ module Searchgasm
6
6
  # Each condition has its own file and class and the source for each condition is pretty self explanatory.
7
7
  class Base
8
8
  include Shared::Utilities
9
- include Shared::Searching
10
9
  include Shared::VirtualClasses
11
10
 
12
11
  attr_accessor :any, :relationship_name
@@ -65,6 +64,8 @@ module Searchgasm
65
64
  end
66
65
 
67
66
  def needed?(model_class, conditions) # :nodoc:
67
+ return false if conditions.blank?
68
+
68
69
  if conditions.is_a?(Hash)
69
70
  return true if conditions[:any]
70
71
  column_names = model_class.column_names
@@ -105,11 +106,11 @@ module Searchgasm
105
106
  end
106
107
 
107
108
  # A list of joins to use when searching, includes relationships
108
- def joins
109
+ def auto_joins
109
110
  j = []
110
111
  associations.each do |association|
111
112
  next if association.conditions.blank?
112
- association_joins = association.joins
113
+ association_joins = association.auto_joins
113
114
  j << (association_joins.blank? ? association.relationship_name.to_sym : {association.relationship_name.to_sym => association_joins})
114
115
  end
115
116
  j.blank? ? nil : (j.size == 1 ? j.first : j)
@@ -262,7 +263,7 @@ module Searchgasm
262
263
  end
263
264
 
264
265
  def reset_objects!
265
- objects.each { |object| eval("@#{object.name} = nil") }
266
+ objects.each { |object| object.class < ::Searchgasm::Conditions::Base ? eval("@#{object.relationship_name} = nil") : eval("@#{object.name} = nil") }
266
267
  objects.clear
267
268
  end
268
269
 
@@ -16,6 +16,19 @@ module Searchgasm
16
16
  new_hash
17
17
  end
18
18
 
19
+ def deep_delete_duplicates(hash)
20
+ hash.each do |k, v|
21
+ if v.is_a?(Hash) && self[k]
22
+ self[k].deep_delete_duplicates(v)
23
+ self.delete(k) if self[k].blank?
24
+ else
25
+ self.delete(k)
26
+ end
27
+ end
28
+
29
+ self
30
+ end
31
+
19
32
  # assert_valid_keys was killing performance. Array.flatten was the culprit, so I rewrote this method, got a 35% performance increase
20
33
  def fast_assert_valid_keys(valid_keys)
21
34
  unknown_keys = keys - valid_keys
@@ -186,35 +186,33 @@ module Searchgasm
186
186
 
187
187
  private
188
188
  def add_order_by_link_defaults!(order_by, options = {})
189
- add_searchgasm_helper_defaults!(:order_by, options)
189
+ add_searchgasm_control_defaults!(:order_by, options)
190
190
  options[:text] ||= determine_order_by_text(order_by)
191
191
  options[:asc_indicator] ||= Config.asc_indicator
192
192
  options[:desc_indicator] ||= Config.desc_indicator
193
193
  options[:text] += options[:search_obj].desc? ? options[:desc_indicator] : options[:asc_indicator] if options[:search_obj].order_by == order_by
194
- url_hash = searchgasm_url_hash(:order_by, order_by, options)
195
- url_hash[:order_as] = (options[:search_obj].order_by == order_by && options[:search_obj].asc?) ? "DESC" : "ASC"
196
- options[:url] = searchgasm_url(url_hash, options)
194
+ options[:url] = searchgasm_params(options.merge(:search_params => {:order_by => order_by}))
197
195
  options
198
196
  end
199
197
 
200
198
  def add_order_as_link_defaults!(order_as, options = {})
201
- add_searchgasm_helper_defaults!(:order_as, options)
199
+ add_searchgasm_control_defaults!(:order_as, options)
202
200
  options[:text] ||= order_as.to_s
203
- options[:url] = searchgasm_url(searchgasm_url_hash(:order_as, order_as, options), options)
201
+ options[:url] = searchgasm_params(options.merge(:search_params => {:order_as => order_as}))
204
202
  options
205
203
  end
206
204
 
207
205
  def add_per_page_link_defaults!(per_page, options = {})
208
- add_searchgasm_helper_defaults!(:per_page, options)
206
+ add_searchgasm_control_defaults!(:per_page, options)
209
207
  options[:text] ||= per_page.blank? ? "Show all" : "#{per_page} per page"
210
- options[:url] = searchgasm_url(searchgasm_url_hash(:per_page, per_page, options), options)
208
+ options[:url] = searchgasm_params(options.merge(:search_params => {:per_page => per_page}))
211
209
  options
212
210
  end
213
211
 
214
212
  def add_page_link_defaults!(page, options = {})
215
- add_searchgasm_helper_defaults!(:page, options)
213
+ add_searchgasm_control_defaults!(:page, options)
216
214
  options[:text] ||= page.to_s
217
- options[:url] = searchgasm_url(searchgasm_url_hash(:page, page, options), options)
215
+ options[:url] = searchgasm_params(options.merge(:search_params => {:page => page}))
218
216
  options
219
217
  end
220
218
 
@@ -148,19 +148,19 @@ module Searchgasm
148
148
 
149
149
  private
150
150
  def add_order_by_links_defaults!(options)
151
- add_searchgasm_helper_defaults!(:order_by, options)
151
+ add_searchgasm_control_defaults!(:order_by, options)
152
152
  options[:choices] ||= options[:search_obj].klass.column_names.map(&:humanize)
153
153
  options
154
154
  end
155
155
 
156
156
  def add_order_as_links_defaults!(options)
157
- add_searchgasm_helper_defaults!(:order_as, options)
157
+ add_searchgasm_control_defaults!(:order_as, options)
158
158
  options[:choices] = [:asc, :desc]
159
159
  options
160
160
  end
161
161
 
162
162
  def add_per_page_links_defaults!(options)
163
- add_searchgasm_helper_defaults!(:per_page, options)
163
+ add_searchgasm_control_defaults!(:per_page, options)
164
164
  options[:choices] ||= Config.per_page_choices.dup
165
165
  if !options[:search_obj].per_page.blank? && !options[:choices].include?(options[:search_obj].per_page)
166
166
  options[:choices] << options[:search_obj].per_page
@@ -173,7 +173,7 @@ module Searchgasm
173
173
  end
174
174
 
175
175
  def add_page_links_defaults!(options)
176
- add_searchgasm_helper_defaults!(:page, options)
176
+ add_searchgasm_control_defaults!(:page, options)
177
177
  options[:first_page] ||= 1
178
178
  options[:last_page] ||= options[:search_obj].page_count
179
179
  options[:current_page] ||= options[:search_obj].page
@@ -13,7 +13,7 @@ module Searchgasm
13
13
 
14
14
  # Please see order_as_links. All options are the same and applicable here. The only difference is that instead of a group of links, this gets returned as a select form element that will perform the same function when the value is changed.
15
15
  def order_as_select(options = {})
16
- add_order_by_select_defaults!(options)
16
+ add_order_as_select_defaults!(options)
17
17
  searchgasm_state_for(:order_as, options) + select(options[:params_scope], :order_as, options[:choices], options[:tag], options[:html])
18
18
  end
19
19
 
@@ -59,19 +59,14 @@ module Searchgasm
59
59
  options[:tag] ||= {}
60
60
  options[:tag][:object] = options[:search_obj]
61
61
 
62
- url = searchgasm_url_hash(option, nil, options)
63
- url.delete(option)
64
- url = searchgasm_url(url, options)
65
- url = url_for(url)
66
- url_option = CGI.escape((options[:params_scope].blank? ? "#{option}" : "#{options[:params_scope]}[#{option}]")) + "='+this.value"
67
- url += (url.last == "?" ? "" : (url.include?("?") ? "&amp;" : "?")) + url_option
62
+ url = searchgasm_url(options.merge(:literal_search_params => {option => "'+this.value+'"}))
68
63
 
69
64
  options[:html][:onchange] ||= ""
70
65
  options[:html][:onchange] += ";"
71
66
  if !options[:remote]
72
- options[:html][:onchange] += "window.location='#{url};"
67
+ options[:html][:onchange] += "window.location='#{url}';"
73
68
  else
74
- options[:html][:onchange] += remote_function(:url => url, :method => :get).gsub(/\\'\+this.value'/, "'+this.value")
69
+ options[:html][:onchange] += remote_function(:url => url, :method => :get).gsub(/\\'\+this.value\+\\'/, "'+this.value+'")
75
70
  end
76
71
  options[:html][:id] ||= ""
77
72
  options
@@ -1,41 +1,63 @@
1
1
  module Searchgasm
2
2
  module Helpers #:nodoc:
3
3
  module Utilities # :nodoc:
4
+ # Builds a hash of params for creating a url.
5
+ def searchgasm_params(options = {})
6
+ add_searchgasm_defaults!(options)
7
+ options[:search_params] ||= {}
8
+ options[:literal_search_params] ||= {}
9
+ options[:params] ||= {}
10
+ params_copy = params.deep_dup.with_indifferent_access
11
+ search_params = options[:params_scope].blank? ? params_copy : params_copy.delete(options[:params_scope])
12
+ search_params ||= {}
13
+ search_params = search_params.with_indifferent_access
14
+ search_params.delete(:commit)
15
+ search_params.delete(:page)
16
+ search_params.deep_delete_duplicates(options[:literal_search_params])
17
+
18
+ if options[:search_params]
19
+ search_params.deep_merge!(options[:search_params])
20
+
21
+ if options[:search_params][:order_by] && !options[:search_params][:order_as]
22
+ search_params[:order_as] = (options[:search_obj].order_by == options[:search_params][:order_by] && options[:search_obj].asc?) ? "DESC" : "ASC"
23
+ end
24
+ end
25
+
26
+ new_params = params_copy
27
+ new_params.deep_merge!(options[:params])
28
+
29
+ if options[:params_scope].blank? || search_params.blank?
30
+ new_params
31
+ else
32
+ new_params.merge(options[:params_scope] => search_params)
33
+ end
34
+ end
35
+
36
+ def searchgasm_url(options = {})
37
+ search_params = searchgasm_params(options)
38
+ url = url_for(search_params)
39
+ literal_param_strings = literal_param_strings(options[:literal_search_params], options[:params_scope].blank? ? "" : "#{options[:params_scope]}")
40
+ url += (url.last == "?" ? "" : (url.include?("?") ? "&amp;" : "?")) + literal_param_strings.join("&amp;")
41
+ end
42
+
4
43
  private
5
44
  # Adds default options for all helper methods.
6
- def add_searchgasm_helper_defaults!(option, options)
45
+ def add_searchgasm_defaults!(options)
7
46
  options[:params_scope] = :search unless options.has_key?(:params_scope)
8
47
  options[:search_obj] ||= instance_variable_get("@#{options[:params_scope]}")
9
48
  raise(ArgumentError, "@search object could not be inferred, please specify: :search_obj => @search or :params_scope => :search_obj_name") unless options[:search_obj].is_a?(Searchgasm::Search::Base)
49
+ options
50
+ end
51
+
52
+ # Adds default options for all control type helper methods.
53
+ def add_searchgasm_control_defaults!(option, options)
54
+ add_searchgasm_defaults!(options)
10
55
  options[:html] ||= {}
11
56
  options[:html][:class] ||= ""
12
57
  searchgasm_add_class!(options[:html], option)
13
58
  options
14
59
  end
15
60
 
16
- def searchgasm_url(url_hash, options)
17
- url = options[:params_scope].blank? ? url_hash : {options[:params_scope] => url_hash}
18
- !options[:url_params].is_a?(Hash) ? url : url.merge(options[:url_params])
19
- end
20
-
21
- def searchgasm_url_hash(option, value, options)
22
- params_copy = params.deep_dup.with_indifferent_access
23
- params_copy.delete(:commit)
24
-
25
- # Extract search params from params
26
- search_params = options[:params_scope].blank? ? params_copy : params_copy[options[:params_scope]]
27
- search_params ||= {}
28
- search_params = search_params.with_indifferent_access
29
-
30
- # Never want to keep page
31
- search_params.delete(:page)
32
-
33
- # Use special order_by value
34
- search_params[option] = option == :order_by ? searchgasm_order_by_value(value) : value
35
-
36
- search_params
37
- end
38
-
39
61
  def searchgasm_add_class!(html_options, new_class)
40
62
  new_class = new_class.to_s
41
63
  classes = html_options[:class].split(" ")
@@ -63,20 +85,22 @@ module Searchgasm
63
85
  html
64
86
  end
65
87
 
66
- # Need to deep dup a hash otherwise the "child" hashes get modified as its passed around
67
- def searchgasm_deep_dup(hash)
68
- new_hash = {}
88
+ def literal_param_strings(literal_params, prefix)
89
+ param_strings = []
69
90
 
70
- hash.each do |k, v|
91
+ literal_params.each do |k, v|
92
+ param_string = prefix.blank? ? k.to_s : "#{prefix}[#{k}]"
71
93
  case v
72
94
  when Hash
73
- hash[k] = searchgasm_deep_dup(v)
95
+ literal_param_strings(v, param_string).each do |literal_param_string|
96
+ param_strings << literal_param_string
97
+ end
74
98
  else
75
- hew_hash[k] = v
99
+ param_strings << (CGI.escape(param_string) + "=#{v}")
76
100
  end
77
101
  end
78
102
 
79
- new_hash
103
+ param_strings
80
104
  end
81
105
  end
82
106
  end
@@ -3,10 +3,8 @@ module Searchgasm #:nodoc:
3
3
  # = Searchgasm
4
4
  #
5
5
  # Please refer the README.rdoc for usage, examples, and installation.
6
-
7
6
  class Base
8
7
  include Searchgasm::Shared::Utilities
9
- include Searchgasm::Shared::Searching
10
8
  include Searchgasm::Shared::VirtualClasses
11
9
 
12
10
  # Options ActiveRecord allows when searching
@@ -24,11 +22,14 @@ module Searchgasm #:nodoc:
24
22
  OPTIONS = SPECIAL_FIND_OPTIONS + AR_OPTIONS # the order is very important, these options get set in this order
25
23
 
26
24
  attr_accessor *AR_OPTIONS
25
+ attr_reader :auto_joins
27
26
 
28
27
  class << self
29
28
  # Used in the ActiveRecord methods to determine if Searchgasm should get involved or not.
30
29
  # This keeps Searchgasm out of the way unless it is needed.
31
30
  def needed?(model_class, options)
31
+ return false if options.blank?
32
+
32
33
  SPECIAL_FIND_OPTIONS.each do |option|
33
34
  return true if options.symbolize_keys.keys.include?(option)
34
35
  end
@@ -52,6 +53,18 @@ module Searchgasm #:nodoc:
52
53
  @acting_as_filter == true
53
54
  end
54
55
 
56
+ # Specific implementation of cloning
57
+ def clone
58
+ options = {}
59
+ (OPTIONS - [:conditions]).each { |option| options[option] = send(option) }
60
+ options[:conditions] = conditions.conditions
61
+ obj = self.class.new(options)
62
+ obj.protect = protected?
63
+ obj.scope = scope
64
+ obj
65
+ end
66
+ alias_method :dup, :clone
67
+
55
68
  # Makes using searchgasm in the console less annoying and keeps the output meaningful and useful
56
69
  def inspect
57
70
  current_find_options = {}
@@ -66,13 +79,8 @@ module Searchgasm #:nodoc:
66
79
  "#<#{klass}Search #{current_find_options.inspect}>"
67
80
  end
68
81
 
69
- def joins=(value)
70
- @memoized_joins = nil
71
- @joins = value
72
- end
73
-
74
82
  def joins
75
- @memoized_joins ||= @joins
83
+ merge_joins(@joins, auto_joins)
76
84
  end
77
85
 
78
86
  def limit=(value)
@@ -116,9 +124,7 @@ module Searchgasm #:nodoc:
116
124
  if searching
117
125
  find_options[:group] ||= "#{quote_table_name(klass.table_name)}.#{quote_column_name(klass.primary_key)}"
118
126
  else
119
- # If we are calculating use includes because they use joins that grab uniq records. When calculating, includes don't have the
120
- # performance hit that they have when searching. Plus it's cleaner.
121
- find_options[:include] = merge_joins(find_options[:include], find_options.delete(:joins))
127
+ find_options[:distinct] = true
122
128
  end
123
129
  end
124
130
 
@@ -10,6 +10,7 @@ module Searchgasm
10
10
  alias_method_chain :initialize, :conditions
11
11
  alias_method_chain :conditions=, :conditions
12
12
  alias_method_chain :conditions, :conditions
13
+ alias_method_chain :auto_joins, :conditions
13
14
  alias_method_chain :joins, :conditions
14
15
  alias_method_chain :sanitize, :conditions
15
16
  end
@@ -34,7 +35,7 @@ module Searchgasm
34
35
  # now you can create the rest of your search and your "scope" will get merged into your final SQL.
35
36
  # What this does is determine if the value a hash or a conditions object, if not it sets it up as a scope.
36
37
  def conditions_with_conditions=(values)
37
- @memoized_joins = nil
38
+ @memoized_auto_joins = nil
38
39
  case values
39
40
  when Searchgasm::Conditions::Base
40
41
  @conditions = values
@@ -44,7 +45,7 @@ module Searchgasm
44
45
  end
45
46
 
46
47
  def conditions_with_conditions
47
- @memoized_joins = nil # have to assume they are calling a condition on a relationship
48
+ @memoized_auto_joins = nil # have to assume they are calling a condition on a relationship
48
49
  conditions_without_conditions
49
50
  end
50
51
 
@@ -52,8 +53,18 @@ module Searchgasm
52
53
  #
53
54
  # <b>Be careful!</b>
54
55
  # ActiveRecord associations can be an SQL train wreck. Make sure you think about what you are searching and that you aren't joining a table with a million records.
56
+ def auto_joins_with_conditions
57
+ @memoized_auto_joins ||= merge_joins(auto_joins_without_conditions, conditions.auto_joins)
58
+ end
59
+
60
+ # Changes joins to use left outer joins if conditions.any == true.
55
61
  def joins_with_conditions
56
- @memoized_joins ||= merge_joins(joins_without_conditions, conditions.joins)
62
+ if conditions.any?
63
+ join_dependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(klass, joins_without_conditions, nil)
64
+ join_dependency.join_associations.collect { |assoc| assoc.association_join }.join
65
+ else
66
+ joins_without_conditions
67
+ end
57
68
  end
58
69
 
59
70
  def sanitize_with_conditions(searching = true) # :nodoc: