searchgasm 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,14 @@
1
+ == 1.2.1 released 2008-09-24
2
+
3
+ * Fixed problem when determining if an order_by_link is currently being ordered. Just "stringified" both comparable values.
4
+ * Removed default order_by and order_as. They will ONLY have values if you specify how to order, otherwise they are nil.
5
+ * Removed order_as requirement. order_as is optional.
6
+ * Added in deep_merge methods for hash, copied over from ActiveSupport 2.1
7
+ * Improved order by auto joins to be based off of what order_by returns instead of setting it when setting order_by.
8
+ * Added priority_order_by. Useful if you want to order featured products first and then order as usual. See documentation in Searchgasm::Search::Ordering for more info.
9
+ * Added in base64 support for order_by and priority_order_by so that it's value is safe in the URL
10
+ * Added priority_order_by_link
11
+
1
12
  == 1.2.0 released 2008-09-24
2
13
 
3
14
  * Added searchgasm_params and searchgasm_url helper to use outside of the control type helpers.
data/Rakefile CHANGED
@@ -10,7 +10,6 @@ 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 = ['activerecord', 'activesupport >= 2.1.0']
13
+ p.dependencies = ['activerecord', 'activesupport']
14
14
  p.include_rakefile = true
15
- end
16
-
15
+ end
@@ -44,7 +44,7 @@ module Searchgasm
44
44
  end
45
45
 
46
46
  def hidden_fields # :nodoc:
47
- @hidden_fields ||= (Search::Base::SPECIAL_FIND_OPTIONS - [:page])
47
+ @hidden_fields ||= (Search::Base::SPECIAL_FIND_OPTIONS - [:page, :priority_order])
48
48
  end
49
49
 
50
50
  # Which hidden fields to automatically include when creating a form with a Searchgasm object. See Searchgasm::Helpers::Form for more info.
@@ -135,7 +135,7 @@ module Searchgasm
135
135
  # The reason for this not to disturb regular queries such as Whatever.find(:all). You would not expect that to be limited.
136
136
  #
137
137
  # * <tt>Default:</tt> The 3rd option in your per_page_choices, default of 50
138
- # * <tt>Accepts:</tt> Any value in your per_page choices, nil means "show all"
138
+ # * <tt>Accepts:</tt> Any value in your per_page choices, nil or a blank string means "show all"
139
139
  def per_page=(value)
140
140
  @per_page = value
141
141
  end
@@ -16,19 +16,53 @@ module Searchgasm
16
16
  new_hash
17
17
  end
18
18
 
19
- def deep_delete_duplicates(hash)
19
+ def deep_delete_duplicate_keys(hash)
20
20
  hash.each do |k, v|
21
21
  if v.is_a?(Hash) && self[k]
22
22
  self[k].deep_delete_duplicates(v)
23
- self.delete(k) if self[k].blank?
23
+ delete(k) if self[k].blank?
24
24
  else
25
- self.delete(k)
25
+ delete(k)
26
26
  end
27
27
  end
28
28
 
29
29
  self
30
30
  end
31
31
 
32
+ def deep_delete(value)
33
+ case value
34
+ when Array
35
+ value.each { |v| deep_delete(v) }
36
+ when Hash
37
+ value.each do |k, v|
38
+ next unless self[k].is_a?(Hash)
39
+
40
+ case v
41
+ when Hash, Array
42
+ self[k].deep_delete(v)
43
+ when String, Symbol
44
+ self[k].delete(v)
45
+ end
46
+ end
47
+ when String, Symbol
48
+ delete(value)
49
+ end
50
+ end
51
+
52
+ def deep_merge(other_hash)
53
+ self.merge(other_hash) do |key, oldval, newval|
54
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
55
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
56
+ oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? oldval.deep_merge(newval) : newval
57
+ end
58
+ end
59
+
60
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
61
+ # Modifies the receiver in place.
62
+ def deep_merge!(other_hash)
63
+ replace(deep_merge(other_hash))
64
+ end
65
+
32
66
  # assert_valid_keys was killing performance. Array.flatten was the culprit, so I rewrote this method, got a 35% performance increase
33
67
  def fast_assert_valid_keys(valid_keys)
34
68
  unknown_keys = keys - valid_keys
@@ -80,7 +80,10 @@ module Searchgasm
80
80
  # === Advanced Options
81
81
  # * <tt>:params_scope</tt> -- default: :search, this is the scope in which your search params will be preserved (params[:search]). If you don't want a scope and want your options to be at base leve in params such as params[:page], params[:per_page], etc, then set this to nil.
82
82
  # * <tt>:search_obj</tt> -- default: @#{params_scope}, this is your search object, everything revolves around this. It will try to infer the name from your params_scope. If your params_scope is :search it will try to get @search, etc. If it can not be inferred by this, you need to pass the object itself.
83
- # * <tt>:url_params</tt> -- default: nil, Additional params to add to the url, must be a hash
83
+ # * <tt>:params</tt> -- default: nil, Additional params to add to the url, must be a hash
84
+ # * <tt>:exclude_params</tt> -- default: nil, params you want to exclude. This is nifty because it does a "deep delete". So you can pass {:param1 => {:param2 => :param3}} and it will make sure param3 does not get include. param1 and param2 will not be touched. This also accepts an array or just a symbol or string.
85
+ # * <tt>:search_params</tt> -- default: nil, Additional search params to add to the url, must be a hash. Adds the options into the :params_scope.
86
+ # * <tt>:exclude_search_params</tt> -- default: nil, Same as :exclude_params but for the :search_params.
84
87
  def order_by_link(order_by, options = {})
85
88
  order_by = deep_stringify(order_by)
86
89
  add_order_by_link_defaults!(order_by, options)
@@ -110,7 +113,10 @@ module Searchgasm
110
113
  # === Advanced Options
111
114
  # * <tt>:params_scope</tt> -- default: :search, this is the scope in which your search params will be preserved (params[:search]). If you don't want a scope and want your options to be at base leve in params such as params[:page], params[:per_page], etc, then set this to nil.
112
115
  # * <tt>:search_obj</tt> -- default: @#{params_scope}, this is your search object, everything revolves around this. It will try to infer the name from your params_scope. If your params_scope is :search it will try to get @search, etc. If it can not be inferred by this, you need to pass the object itself.
113
- # * <tt>:url_params</tt> -- default: nil, Additional params to add to the url, must be a hash
116
+ # * <tt>:params</tt> -- default: nil, Additional params to add to the url, must be a hash
117
+ # * <tt>:exclude_params</tt> -- default: nil, params you want to exclude. This is nifty because it does a "deep delete". So you can pass {:param1 => {:param2 => :param3}} and it will make sure param3 does not get include. param1 and param2 will not be touched. This also accepts an array or just a symbol or string.
118
+ # * <tt>:search_params</tt> -- default: nil, Additional search params to add to the url, must be a hash. Adds the options into the :params_scope.
119
+ # * <tt>:exclude_search_params</tt> -- default: nil, Same as :exclude_params but for the :search_params.
114
120
  def order_as_link(order_as, options = {})
115
121
  add_order_as_link_defaults!(order_as, options)
116
122
  html = searchgasm_state_for(:order_as, options)
@@ -124,6 +130,48 @@ module Searchgasm
124
130
  html
125
131
  end
126
132
 
133
+ # This is similar to order_by_link but with a small difference. The best way to explain priority ordering is with an example. Let's say you wanted to list products on a page. You have "featured" products
134
+ # that you want to show up first, no matter what. This is what this is all about. It makes ordering by featured products a priority, then searching by price, quantity, etc. is the same as it has always been.
135
+ #
136
+ # The difference between order_by_link and priority_order_by_link is that priority_order_by_link it just a switch. Turn it on or turn it off. You don't neccessarily want to flip between ASC and DESC. If you do
137
+ # then you should just incorporate this into your regular order_by, like: order_by_link [:featured, :price]
138
+ #
139
+ # === Example uses for a User class that has many orders
140
+ #
141
+ # priority_order_by_link(:featured, "DESC")
142
+ # order_by_link([:featured, :created_at], "ASC")
143
+ # order_by_link({:orders => :featured}, "ASC")
144
+ # order_by_link([{:orders => :featured}, :featured], "ASC")
145
+ # order_by_link(:featured, "ASC", :text => "Featured", :html => {:class => "featured_link"})
146
+ #
147
+ # === Options
148
+ # * <tt>:activate_text</tt> -- default: "Show #{column_name.to_s.humanize} first"
149
+ # * <tt>:deactivate_text</tt> -- default: "Don't show #{column_name.to_s.humanize} first", text for the link, text for the link
150
+ # * <tt>:column_name</tt> -- default: column_name.to_s.humanize, automatically inferred by what you are ordering by and is added into the active_text and deactive_text strings.
151
+ # * <tt>:text</tt> -- default: :activate_text or :deactivate_text depending on if its active or not, Overwriting this will make this text stay the same, no matter way. A good alternative would be "Toggle featured first"
152
+ # * <tt>:html</tt> -- html arrtributes for the <a> tag.
153
+ #
154
+ # === Advanced Options
155
+ # * <tt>:params_scope</tt> -- default: :search, this is the scope in which your search params will be preserved (params[:search]). If you don't want a scope and want your options to be at base leve in params such as params[:page], params[:per_page], etc, then set this to nil.
156
+ # * <tt>:search_obj</tt> -- default: @#{params_scope}, this is your search object, everything revolves around this. It will try to infer the name from your params_scope. If your params_scope is :search it will try to get @search, etc. If it can not be inferred by this, you need to pass the object itself.
157
+ # * <tt>:params</tt> -- default: nil, Additional params to add to the url, must be a hash
158
+ # * <tt>:exclude_params</tt> -- default: nil, params you want to exclude. This is nifty because it does a "deep delete". So you can pass {:param1 => {:param2 => :param3}} and it will make sure param3 does not get include. param1 and param2 will not be touched. This also accepts an array or just a symbol or string.
159
+ # * <tt>:search_params</tt> -- default: nil, Additional search params to add to the url, must be a hash. Adds the options into the :params_scope.
160
+ # * <tt>:exclude_search_params</tt> -- default: nil, Same as :exclude_params but for the :search_params.
161
+ def priority_order_by_link(priority_order_by, priority_order_as, options = {})
162
+ priority_order_by = deep_stringify(priority_order_by)
163
+ add_priority_order_by_link_defaults!(priority_order_by, priority_order_as, options)
164
+ html = searchgasm_state_for(:priority_order_by, options) + searchgasm_state_for(:priority_order_as, options)
165
+
166
+ if !options[:is_remote]
167
+ html += link_to(options[:text], options[:url], options[:html])
168
+ else
169
+ html += link_to_remote(options[:text], options[:remote].merge(:url => options[:url]), options[:html])
170
+ end
171
+
172
+ html
173
+ end
174
+
127
175
  # Creates a link for limiting how many items are on each page
128
176
  #
129
177
  # === Example uses
@@ -141,7 +189,10 @@ module Searchgasm
141
189
  # === Advanced Options
142
190
  # * <tt>:params_scope</tt> -- default: :search, this is the scope in which your search params will be preserved (params[:search]). If you don't want a scope and want your options to be at base leve in params such as params[:page], params[:per_page], etc, then set this to nil.
143
191
  # * <tt>:search_obj</tt> -- default: @#{params_scope}, this is your search object, everything revolves around this. It will try to infer the name from your params_scope. If your params_scope is :search it will try to get @search, etc. If it can not be inferred by this, you need to pass the object itself.
144
- # * <tt>:url_params</tt> -- default: nil, Additional params to add to the url, must be a hash
192
+ # * <tt>:params</tt> -- default: nil, Additional params to add to the url, must be a hash
193
+ # * <tt>:exclude_params</tt> -- default: nil, params you want to exclude. This is nifty because it does a "deep delete". So you can pass {:param1 => {:param2 => :param3}} and it will make sure param3 does not get include. param1 and param2 will not be touched. This also accepts an array or just a symbol or string.
194
+ # * <tt>:search_params</tt> -- default: nil, Additional search params to add to the url, must be a hash. Adds the options into the :params_scope.
195
+ # * <tt>:exclude_search_params</tt> -- default: nil, Same as :exclude_params but for the :search_params.
145
196
  def per_page_link(per_page, options = {})
146
197
  add_per_page_link_defaults!(per_page, options)
147
198
  html = searchgasm_state_for(:per_page, options)
@@ -170,7 +221,10 @@ module Searchgasm
170
221
  # === Advanced Options
171
222
  # * <tt>:params_scope</tt> -- default: :search, this is the scope in which your search params will be preserved (params[:search]). If you don't want a scope and want your options to be at base leve in params such as params[:page], params[:per_page], etc, then set this to nil.
172
223
  # * <tt>:search_obj</tt> -- default: @#{params_scope}, this is your search object, everything revolves around this. It will try to infer the name from your params_scope. If your params_scope is :search it will try to get @search, etc. If it can not be inferred by this, you need to pass the object itself.
173
- # * <tt>:url_params</tt> -- default: nil, Additional params to add to the url, must be a hash
224
+ # * <tt>:params</tt> -- default: nil, Additional params to add to the url, must be a hash
225
+ # * <tt>:exclude_params</tt> -- default: nil, params you want to exclude. This is nifty because it does a "deep delete". So you can pass {:param1 => {:param2 => :param3}} and it will make sure param3 does not get include. param1 and param2 will not be touched. This also accepts an array or just a symbol or string.
226
+ # * <tt>:search_params</tt> -- default: nil, Additional search params to add to the url, must be a hash. Adds the options into the :params_scope.
227
+ # * <tt>:exclude_search_params</tt> -- default: nil, Same as :exclude_params but for the :search_params.
174
228
  def page_link(page, options = {})
175
229
  add_page_link_defaults!(page, options)
176
230
  html = searchgasm_state_for(:page, options)
@@ -190,7 +244,7 @@ module Searchgasm
190
244
  options[:text] ||= determine_order_by_text(order_by)
191
245
  options[:asc_indicator] ||= Config.asc_indicator
192
246
  options[:desc_indicator] ||= Config.desc_indicator
193
- options[:text] += options[:search_obj].desc? ? options[:desc_indicator] : options[:asc_indicator] if options[:search_obj].order_by == order_by
247
+ options[:text] += options[:search_obj].desc? ? options[:desc_indicator] : options[:asc_indicator] if deep_stringify(options[:search_obj].order_by) == order_by
194
248
  options[:url] = searchgasm_params(options.merge(:search_params => {:order_by => order_by}))
195
249
  options
196
250
  end
@@ -202,6 +256,22 @@ module Searchgasm
202
256
  options
203
257
  end
204
258
 
259
+ def add_priority_order_by_link_defaults!(priority_order_by, priority_order_as, options = {})
260
+ add_searchgasm_control_defaults!(:priority_order_by, options)
261
+ options[:column_name] ||= determine_order_by_text(priority_order_by).downcase
262
+ options[:activate_text] ||= "Show #{options[:column_name]} first"
263
+ options[:deactivate_text] ||= "Don't show #{options[:column_name]} first"
264
+ active = deep_stringify(options[:search_obj].priority_order_by) == priority_order_by && options[:search_obj].priority_order_as == priority_order_as
265
+ options[:text] ||= active ? options[:deactivate_text] : options[:activate_text]
266
+ if active
267
+ options.merge!(:search_params => {:priority_order_by => nil, :priority_order_as => nil})
268
+ else
269
+ options.merge!(:search_params => {:priority_order_by => priority_order_by, :priority_order_as => priority_order_as})
270
+ end
271
+ options[:url] = searchgasm_params(options)
272
+ options
273
+ end
274
+
205
275
  def add_per_page_link_defaults!(per_page, options = {})
206
276
  add_searchgasm_control_defaults!(:per_page, options)
207
277
  options[:text] ||= per_page.blank? ? "Show all" : "#{per_page} per page"
@@ -234,13 +304,15 @@ module Searchgasm
234
304
  when String
235
305
  obj
236
306
  when Symbol
237
- obj = obj.to_s
307
+ obj.to_s
238
308
  when Array
239
- obj = obj.collect { |item| deep_stringify(item) }
309
+ obj.collect { |item| deep_stringify(item) }
240
310
  when Hash
241
311
  new_obj = {}
242
312
  obj.each { |key, value| new_obj[key.to_s] = deep_stringify(value) }
243
313
  new_obj
314
+ else
315
+ obj
244
316
  end
245
317
  end
246
318
  end
@@ -23,7 +23,7 @@ module Searchgasm
23
23
  per_page_select(options)
24
24
  end
25
25
 
26
- # Please see page_links. All options are the same and applicable here, excep the :prev, :next, :first, and :last options. 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.
26
+ # Please see page_links. All options are the same and applicable here, except the :prev, :next, :first, and :last options. 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.
27
27
  def remote_page_select(options = {})
28
28
  add_remote_defaults!(options)
29
29
  page_select(options)
@@ -122,7 +122,7 @@ module Searchgasm
122
122
  options = args.extract_options!
123
123
  options
124
124
  search_options[:hidden_fields].each do |field|
125
- html = hidden_field(name, field, :object => search_object, :id => "#{name}_#{field}_hidden", :value => (field == :order_by ? searchgasm_order_by_value(search_object.order_by) : search_object.send(field)))
125
+ html = hidden_field(name, field, :object => search_object, :id => "#{name}_#{field}_hidden", :value => (field == :order_by ? searchgasm_base64_value(search_object.order_by) : search_object.send(field)))
126
126
 
127
127
  # For edge rails and older version compatibility, passing a binding to concat was deprecated
128
128
  begin
@@ -1,11 +1,31 @@
1
1
  module Searchgasm
2
2
  module Helpers #:nodoc:
3
3
  module Utilities # :nodoc:
4
- # Builds a hash of params for creating a url.
4
+ # Builds a hash of params for creating a url and preserves any existing params. You can pass this into url_for and build your url. Although most rails helpers accept a hash.
5
+ #
6
+ # Let's take the page_link helper. Here is the code behind that helper:
7
+ #
8
+ # link_to("Page 2", searchgasm_params(:search_params => {:page => 2}))
9
+ #
10
+ # That's pretty much it. So if you wanted to roll your own link to execute a search, go for it. It's pretty simple. Pass conditions instead of the page, set how the search will be ordered, etc.
11
+ #
12
+ # <b>Be careful</b> when taking this approach though. Searchgasm helps you out when you use form_for. For example, when you use the per_page_select helper, it adds in a hidden form field with the value of the page. So when
13
+ # your search form is submitted it searches the document for that element, finds the current value, which is the current per_page value, and includes that in the search. So when a user searches the per_page
14
+ # value stays consistent. If you use the searchgasm_params you are on your own. I am always curious how people are using searchgasm. So if you are building your own helpers contact me and maybe I can help you
15
+ # and add in a helper for you, making it an *official* feature.
16
+ #
17
+ # === Options
18
+ # * <tt>:params_scope</tt> -- default: :search, this is the scope in which your search params will be preserved (params[:search]). If you don't want a scope and want your options to be at base leve in params such as params[:page], params[:per_page], etc, then set this to nil.
19
+ # * <tt>:search_obj</tt> -- default: @#{params_scope}, this is your search object, everything revolves around this. It will try to infer the name from your params_scope. If your params_scope is :search it will try to get @search, etc. If it can not be inferred by this, you need to pass the object itself.
20
+ # * <tt>:params</tt> -- default: nil, Additional params to add to the url, must be a hash
21
+ # * <tt>:exclude_params</tt> -- default: nil, params you want to exclude. This is nifty because it does a "deep delete". So you can pass {:param1 => {:param2 => :param3}} and it will make sure param3 does not get include. param1 and param2 will not be touched. This also accepts an array or just a symbol or string.
22
+ # * <tt>:search_params</tt> -- default: nil, Additional search params to add to the url, must be a hash. Adds the options into the :params_scope.
23
+ # * <tt>:exclude_search_params</tt> -- default: nil, Same as :exclude_params but for the :search_params.
5
24
  def searchgasm_params(options = {})
6
25
  add_searchgasm_defaults!(options)
7
26
  options[:search_params] ||= {}
8
27
  options[:literal_search_params] ||= {}
28
+
9
29
  options[:params] ||= {}
10
30
  params_copy = params.deep_dup.with_indifferent_access
11
31
  search_params = options[:params_scope].blank? ? params_copy : params_copy.delete(options[:params_scope])
@@ -13,18 +33,22 @@ module Searchgasm
13
33
  search_params = search_params.with_indifferent_access
14
34
  search_params.delete(:commit)
15
35
  search_params.delete(:page)
16
- search_params.deep_delete_duplicates(options[:literal_search_params])
36
+ search_params.deep_delete_duplicate_keys(options[:literal_search_params])
37
+ search_params.deep_delete(options[:exclude_search_params])
17
38
 
18
39
  if options[:search_params]
19
40
  search_params.deep_merge!(options[:search_params])
20
41
 
21
42
  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"
43
+ search_params[:order_as] = (options[:search_obj].order_by == options[:search_params][:order_by] && options[:search_obj].asc?) ? "DESC" : "ASC"
23
44
  end
45
+
46
+ [:order_by, :priority_order_by].each { |base64_field| search_params[base64_field] = searchgasm_base64_value(search_params[base64_field]) if search_params.has_key?(base64_field) }
24
47
  end
25
48
 
26
49
  new_params = params_copy
27
50
  new_params.deep_merge!(options[:params])
51
+ new_params.deep_delete(options[:exclude_params])
28
52
 
29
53
  if options[:params_scope].blank? || search_params.blank?
30
54
  new_params
@@ -33,6 +57,32 @@ module Searchgasm
33
57
  end
34
58
  end
35
59
 
60
+ # Similar to searchgasm_hash, but instead returns a string url. The reason this exists is to assist in creating urls in javascript. It's the muscle behind all of the select helpers that searchgasm provides.
61
+ # Take the instance where you want to do:
62
+ #
63
+ # :onchange => "window.location = '#{url_for(searchgasm_params)}&my_param=' + this.value;"
64
+ #
65
+ # Well the above obviously won't work. Do you need to apped the url with a ? or a &? What about that tricky :params_scope? That's where this is handy, beacuse it does all of the params string building for you. Check it out:
66
+ #
67
+ # :onchange => "window.location = '" + searchgasm_url(:literal_search_params => {:per_page => "' + escape(this.value) + '"}) + "';"
68
+ #
69
+ # or what about something a little more tricky?
70
+ #
71
+ # :onchange => "window.location = '" + searchgasm_url(:literal_search_params => {:conditions => {:name_contains => "' + escape(this.value) + '"}}) + "';"
72
+ #
73
+ # I have personally used this for an event calendar. Above the calendar there was a drop down for each month. Here is the code:
74
+ #
75
+ # :onchange => "window.location = '" + searchgasm_url(:literal_search_params => {:conditions => {:occurs_at_after => "' + escape(this.value) + '"}}) + "';"
76
+ #
77
+ # Now when the user changes the month in the drop down it just runs a new search that sets my conditions to occurs_at_after = selected month. Then in my controller I set occurs_at_before = occurs_at_after.at_end_of_month.
78
+ #
79
+ # === Options
80
+ # * <tt>:params_scope</tt> -- default: :search, this is the scope in which your search params will be preserved (params[:search]). If you don't want a scope and want your options to be at base leve in params such as params[:page], params[:per_page], etc, then set this to nil.
81
+ # * <tt>:search_obj</tt> -- default: @#{params_scope}, this is your search object, everything revolves around this. It will try to infer the name from your params_scope. If your params_scope is :search it will try to get @search, etc. If it can not be inferred by this, you need to pass the object itself.
82
+ # * <tt>:params</tt> -- default: nil, Additional params to add to the url, must be a hash
83
+ # * <tt>:exclude_params</tt> -- default: nil, params you want to exclude. This is nifty because it does a "deep delete". So you can pass {:param1 => {:param2 => :param3}} and it will make sure param3 does not get include. param1 and param2 will not be touched. This also accepts an array or just a symbol or string.
84
+ # * <tt>:search_params</tt> -- default: nil, Additional search params to add to the url, must be a hash. Adds the options into the :params_scope.
85
+ # * <tt>:exclude_search_params</tt> -- default: nil, Same as :exclude_params but for the :search_params.
36
86
  def searchgasm_url(options = {})
37
87
  search_params = searchgasm_params(options)
38
88
  url = url_for(search_params)
@@ -65,7 +115,7 @@ module Searchgasm
65
115
  html_options[:class] = classes.join(" ")
66
116
  end
67
117
 
68
- def searchgasm_order_by_value(order_by)
118
+ def searchgasm_base64_value(order_by)
69
119
  case order_by
70
120
  when String
71
121
  order_by
@@ -79,7 +129,7 @@ module Searchgasm
79
129
  html = ""
80
130
  unless @added_state_for.include?(option)
81
131
  value = options[:search_obj].send(option)
82
- html = hidden_field(options[:params_scope], option, :value => (option == :order_by ? searchgasm_order_by_value(value) : value))
132
+ html = hidden_field(options[:params_scope], option, :value => (option == :order_by ? searchgasm_base64_value(value) : value))
83
133
  @added_state_for << option
84
134
  end
85
135
  html
@@ -16,7 +16,7 @@ module Searchgasm #:nodoc:
16
16
  AR_OPTIONS = (AR_FIND_OPTIONS + AR_CALCULATIONS_OPTIONS).uniq
17
17
 
18
18
  # Options that ActiveRecord doesn't suppport, but Searchgasm does
19
- SPECIAL_FIND_OPTIONS = [:order_by, :order_as, :page, :per_page]
19
+ SPECIAL_FIND_OPTIONS = [:order_by, :order_as, :page, :per_page, :priority_order, :priority_order_by, :priority_order_as]
20
20
 
21
21
  # Valid options you can use when searching
22
22
  OPTIONS = SPECIAL_FIND_OPTIONS + AR_OPTIONS # the order is very important, these options get set in this order
@@ -100,13 +100,7 @@ module Searchgasm #:nodoc:
100
100
  def options=(values)
101
101
  return unless values.is_a?(Hash)
102
102
  values.symbolize_keys.fast_assert_valid_keys(OPTIONS)
103
-
104
- OPTIONS.each do |option|
105
- next unless values.has_key?(option)
106
- send("#{option}=", values[option])
107
- end
108
-
109
- values
103
+ values.each { |key, value| send("#{key}=", value) }
110
104
  end
111
105
 
112
106
  # Sanitizes everything down into options ActiveRecord::Base.find can understand
@@ -20,75 +20,67 @@ module Searchgasm
20
20
  klass.class_eval do
21
21
  alias_method_chain :auto_joins, :ordering
22
22
  alias_method_chain :order=, :ordering
23
+ alias_method_chain :sanitize, :ordering
24
+ attr_reader :priority_order
23
25
  end
24
26
  end
25
27
 
26
28
  def auto_joins_with_ordering # :nodoc:
27
- @memoized_auto_joins ||= merge_joins(auto_joins_without_ordering, order_by_auto_joins)
29
+ @memoized_auto_joins ||= merge_joins(auto_joins_without_ordering, order_by_auto_joins, priority_order_by_auto_joins)
28
30
  end
29
31
 
30
32
  def order_with_ordering=(value) # :nodoc
31
33
  @order_by = nil
32
34
  @order_as = nil
33
- self.order_by_auto_joins.clear
35
+ @order_by_auto_joins = nil
34
36
  @memoized_auto_joins = nil
35
37
  self.order_without_ordering = value
36
38
  end
37
39
 
38
40
  # Convenience method for determining if the ordering is ascending
39
41
  def asc?
42
+ return false if order_as.nil?
40
43
  !desc?
41
44
  end
42
45
 
43
46
  # Convenience method for determining if the ordering is descending
44
47
  def desc?
48
+ return false if order_as.nil?
45
49
  order_as == "DESC"
46
50
  end
47
51
 
48
52
  # Determines how the search is being ordered: as DESC or ASC
49
53
  def order_as
50
- @order_as ||= (order.blank? || order =~ /ASC$/i) ? "ASC" : "DESC"
54
+ return if order.blank?
55
+ return @order_as if @order_as
56
+
57
+ case order
58
+ when /ASC$/i
59
+ @order_as = "ASC"
60
+ when /DESC$/i
61
+ @order_as = "DESC"
62
+ else
63
+ nil
64
+ end
51
65
  end
52
66
 
53
67
  # Sets how the results will be ordered: ASC or DESC
54
68
  def order_as=(value)
55
- value = value.to_s.upcase
56
- raise(ArgumentError, "order_as only accepts a string as ASC or DESC") unless ["ASC", "DESC"].include?(value)
57
-
58
- if order.blank?
59
- @order = order_by_to_order(order_by, value)
60
- else
69
+ value = value.blank? ? nil : value.to_s.upcase
70
+ raise(ArgumentError, "order_as only accepts a blank string / nil or a string as 'ASC' or 'DESC'") if !value.blank? && !["ASC", "DESC"].include?(value)
71
+ if @order_by
72
+ @order = order_by_to_order(@order_by, value)
73
+ elsif order
61
74
  @order.gsub!(/(ASC|DESC)/i, value)
62
75
  end
63
-
64
76
  @order_as = value
65
77
  end
66
78
 
67
79
  # Determines by what columns the search is being ordered. This is nifty in that is reverse engineers the order SQL to determine this, only
68
80
  # if you haven't explicitly set the order_by option yourself.
69
81
  def order_by
70
- return @order_by if @order_by
71
-
72
- if !order.blank?
73
- # Reversege engineer order, only go 1 level deep with relationships, anything beyond that is probably excessive and not good for performance
74
- order_parts = order.split(",").collect do |part|
75
- part.strip!
76
- part.gsub!(/ (ASC|DESC)$/i, "").gsub!(/(.*)\./, "")
77
- table_name = ($1 ? $1.gsub(/[^a-z0-9_]/i, "") : nil)
78
- part.gsub!(/[^a-z0-9_]/i, "")
79
- reflection = nil
80
- if table_name && table_name != klass.table_name
81
- reflection = klass.reflect_on_association(table_name.to_sym) || klass.reflect_on_association(table_name.singularize.to_sym)
82
- next unless reflection
83
- {reflection.name.to_s => part}
84
- else
85
- part
86
- end
87
- end.compact
88
- @order_by = order_parts.size <= 1 ? order_parts.first : order_parts
89
- else
90
- @order_by = klass.primary_key
91
- end
82
+ return if order.blank?
83
+ @order_by ||= order_to_order_by(order)
92
84
  end
93
85
 
94
86
  # Lets you set how to order the data
@@ -101,23 +93,86 @@ module Searchgasm
101
93
  # order_by = [:id, name] # => users.id ASC, user.name ASC
102
94
  # order_by = [:id, {:user_group => :name}] # => users.id ASC, user_groups.name ASC
103
95
  def order_by=(value)
104
- self.order_by_auto_joins.clear
96
+ @order_by_auto_joins = nil
105
97
  @memoized_auto_joins = nil
106
98
  @order_by = get_order_by_value(value)
107
- @order = order_by_to_order(@order_by, order_as)
99
+ @order = order_by_to_order(@order_by, @order_as)
108
100
  @order_by
109
101
  end
110
102
 
111
103
  # Returns the joins neccessary for the "order" statement so that we don't get an SQL error
112
104
  def order_by_auto_joins
113
- @order_by_auto_joins ||= []
114
- @order_by_auto_joins.compact!
115
- @order_by_auto_joins.uniq!
116
- @order_by_auto_joins
105
+ @order_by_auto_joins ||= build_order_by_auto_joins(order_by)
106
+ end
107
+
108
+ # Let's you set a priority order. Meaning this will get ordered first before anything else, but is unnoticeable and abstracted out from your regular order. For example, lets say you have a model called Product
109
+ # that had a "featured" boolean column. You want to order the products by the price, quantity, etc., but you want the featured products to always be first.
110
+ #
111
+ # Without a priority order your controller would get cluttered and your code would be much more complicated. All of your order_by_link methods would have to be order_by_link [:featured, :price], :text => "Price"
112
+ # Your order_by_link methods alternate between ASC and DESC, so the featured products would jump from the top the bottom. It presents a lot of "work arounds". So priority_order solves this.
113
+ def priority_order=(value)
114
+ @priority_order = value
115
+ end
116
+
117
+ # Same as order_by but for your priority order. See priority_order= for more informaton on priority_order.
118
+ def priority_order_by
119
+ return if priority_order.blank?
120
+ @priority_order_by ||= order_to_order_by(priority_order)
121
+ end
122
+
123
+ # Same as order_by= but for your priority order. See priority_order= for more informaton on priority_order.
124
+ def priority_order_by=(value)
125
+ @priority_order_by_auto_joins = nil
126
+ @memoized_auto_joins = nil
127
+ @priority_order_by = get_order_by_value(value)
128
+ @priority_order = order_by_to_order(@priority_order_by, @priority_order_as)
129
+ @priority_order_by
130
+ end
131
+
132
+ # Same as order_as but for your priority order. See priority_order= for more informaton on priority_order.
133
+ def priority_order_as
134
+ return if priority_order.blank?
135
+ return @priority_order_as if @priority_order_as
136
+
137
+ case priority_order
138
+ when /ASC$/i
139
+ @priority_order_as = "ASC"
140
+ when /DESC$/i
141
+ @priority_order_as = "DESC"
142
+ else
143
+ nil
144
+ end
145
+ end
146
+
147
+ # Same as order_as= but for your priority order. See priority_order= for more informaton on priority_order.
148
+ def priority_order_as=(value)
149
+ value = value.blank? ? nil : value.to_s.upcase
150
+ raise(ArgumentError, "priority_order_as only accepts a blank string / nil or a string as 'ASC' or 'DESC'") if !value.blank? && !["ASC", "DESC"].include?(value)
151
+ if @priority_order_by
152
+ @priority_order = order_by_to_order(@priority_order_by, value)
153
+ elsif priority_order
154
+ @priority_order.gsub!(/(ASC|DESC)/i, value)
155
+ end
156
+ @priority_order_as = value
157
+ end
158
+
159
+ def priority_order_by_auto_joins
160
+ @priority_order_by_auto_joins ||= build_order_by_auto_joins(priority_order_by)
161
+ end
162
+
163
+ def sanitize_with_ordering(searching = true)
164
+ find_options = sanitize_without_ordering(searching)
165
+ unless priority_order.blank?
166
+ order_parts = [priority_order, find_options[:order]].compact
167
+ find_options[:order] = order_parts.join(", ")
168
+ end
169
+ find_options
117
170
  end
118
171
 
119
172
  private
120
- def order_by_to_order(order_by, order_as, alt_klass = nil, new_joins = [])
173
+ def order_by_to_order(order_by, order_as, alt_klass = nil)
174
+ return if order_by.blank?
175
+
121
176
  k = alt_klass || klass
122
177
  table_name = k.table_name
123
178
  sql_parts = []
@@ -130,23 +185,51 @@ module Searchgasm
130
185
  key = order_by.keys.first
131
186
  reflection = k.reflect_on_association(key.to_sym)
132
187
  value = order_by.values.first
133
- new_joins << key.to_sym
134
- sql_parts << order_by_to_order(value, order_as, reflection.klass, new_joins)
188
+ sql_parts << order_by_to_order(value, order_as, reflection.klass)
135
189
  when Symbol, String
136
- new_join = build_order_by_auto_joins(new_joins)
137
- self.order_by_auto_joins << new_join if new_join
138
- sql_parts << "#{quote_table_name(table_name)}.#{quote_column_name(order_by)} #{order_as}"
190
+ part = "#{quote_table_name(table_name)}.#{quote_column_name(order_by)}"
191
+ part += " #{order_as}" unless order_as.blank?
192
+ sql_parts << part
139
193
  end
140
194
 
141
195
  sql_parts.join(", ")
142
196
  end
143
197
 
144
- def build_order_by_auto_joins(joins)
145
- return joins.first if joins.size <= 1
146
- joins = joins.dup
147
-
148
- key = joins.shift
149
- {key => build_order_by_auto_joins(joins)}
198
+ def order_to_order_by(order)
199
+ # Reversege engineer order, only go 1 level deep with relationships, anything beyond that is probably excessive and not good for performance
200
+ order_parts = order.split(",").collect do |part|
201
+ part.strip!
202
+ part.gsub!(/ (ASC|DESC)$/i, "").gsub!(/(.*)\./, "")
203
+ table_name = ($1 ? $1.gsub(/[^a-z0-9_]/i, "") : nil)
204
+ part.gsub!(/[^a-z0-9_]/i, "")
205
+ reflection = nil
206
+ if table_name && table_name != klass.table_name
207
+ reflection = klass.reflect_on_association(table_name.to_sym) || klass.reflect_on_association(table_name.singularize.to_sym)
208
+ next unless reflection
209
+ {reflection.name.to_s => part}
210
+ else
211
+ part
212
+ end
213
+ end.compact
214
+ order_parts.size <= 1 ? order_parts.first : order_parts
215
+ end
216
+
217
+ def build_order_by_auto_joins(order_by_value)
218
+ case order_by_value
219
+ when Array
220
+ order_by_value.collect { |value| build_order_by_auto_joins(value) }.uniq.compact
221
+ when Hash
222
+ key = order_by_value.keys.first
223
+ value = order_by_value.values.first
224
+ case value
225
+ when Hash
226
+ {key => build_order_by_auto_joins(value)}
227
+ else
228
+ key
229
+ end
230
+ else
231
+ nil
232
+ end
150
233
  end
151
234
 
152
235
  def get_order_by_value(value)
@@ -22,11 +22,11 @@ module Searchgasm
22
22
  # User.all(params[:search])
23
23
  module Protection
24
24
  # Options that are allowed when protecting against SQL injections (still checked though)
25
- SAFE_OPTIONS = Base::SPECIAL_FIND_OPTIONS + [:conditions, :limit, :offset]
25
+ SAFE_OPTIONS = Base::SPECIAL_FIND_OPTIONS + [:conditions, :limit, :offset] - [:priority_order]
26
26
 
27
- VULNERABLE_FIND_OPTIONS = Base::AR_FIND_OPTIONS - SAFE_OPTIONS
27
+ VULNERABLE_FIND_OPTIONS = Base::AR_FIND_OPTIONS - SAFE_OPTIONS + [:priority_order]
28
28
 
29
- VULNERABLE_CALCULATIONS_OPTIONS = Base::AR_CALCULATIONS_OPTIONS - SAFE_OPTIONS
29
+ VULNERABLE_CALCULATIONS_OPTIONS = Base::AR_CALCULATIONS_OPTIONS - SAFE_OPTIONS + [:priority_order]
30
30
 
31
31
  # Options that are not allowed, at all, when protecting against SQL injections
32
32
  VULNERABLE_OPTIONS = Base::OPTIONS - SAFE_OPTIONS
@@ -67,7 +67,7 @@ module Searchgasm
67
67
 
68
68
  MAJOR = 1
69
69
  MINOR = 2
70
- TINY = 0
70
+ TINY = 1
71
71
 
72
72
  # The current version as a Version instance
73
73
  CURRENT = new(MAJOR, MINOR, TINY)
data/searchgasm.gemspec CHANGED
@@ -1,18 +1,18 @@
1
1
 
2
- # Gem::Specification for Searchgasm-1.2.0
2
+ # Gem::Specification for Searchgasm-1.2.1
3
3
  # Originally generated by Echoe
4
4
 
5
5
  --- !ruby/object:Gem::Specification
6
6
  name: searchgasm
7
7
  version: !ruby/object:Gem::Version
8
- version: 1.2.0
8
+ version: 1.2.1
9
9
  platform: ruby
10
10
  authors:
11
11
  - Ben Johnson of Binary Logic
12
12
  autorequire:
13
13
  bindir: bin
14
14
 
15
- date: 2008-09-24 00:00:00 -04:00
15
+ date: 2008-09-26 00:00:00 -04:00
16
16
  default_executable:
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
@@ -34,9 +34,6 @@ dependencies:
34
34
  - - ">="
35
35
  - !ruby/object:Gem::Version
36
36
  version: "0"
37
- - - "="
38
- - !ruby/object:Gem::Version
39
- version: 2.1.0
40
37
  version:
41
38
  - !ruby/object:Gem::Dependency
42
39
  name: echoe
@@ -1,49 +1,22 @@
1
1
  require File.dirname(__FILE__) + '/test_helper.rb'
2
2
 
3
3
  class TestSearchOrdering < Test::Unit::TestCase
4
- def test_order_as
5
- search = Account.new_search
6
- assert_equal nil, search.order
7
- assert_equal "ASC", search.order_as
8
- assert search.asc?
9
-
10
- search.order_as = "DESC"
11
- assert_equal "DESC", search.order_as
12
- assert search.desc?
13
- assert_equal "\"accounts\".\"id\" DESC", search.order
14
-
15
- search.order = "id ASC"
16
- assert_equal "ASC", search.order_as
17
- assert search.asc?
18
- assert_equal "id ASC", search.order
19
-
20
- search.order = "id DESC"
21
- assert_equal "DESC", search.order_as
22
- assert search.desc?
23
- assert_equal "id DESC", search.order
24
-
25
- search.order_by = "name"
26
- assert_equal "DESC", search.order_as
27
- assert search.desc?
28
- assert_equal "\"accounts\".\"name\" DESC", search.order
29
- end
30
-
31
4
  def test_order_by
32
5
  search = Account.new_search
33
6
  assert_equal nil, search.order
34
- assert_equal "id", search.order_by
7
+ assert_equal nil, search.order_by
35
8
 
36
9
  search.order_by = "first_name"
37
10
  assert_equal "first_name", search.order_by
38
- assert_equal "\"accounts\".\"first_name\" ASC", search.order
11
+ assert_equal "\"accounts\".\"first_name\"", search.order
39
12
 
40
13
  search.order_by = "last_name"
41
14
  assert_equal "last_name", search.order_by
42
- assert_equal "\"accounts\".\"last_name\" ASC", search.order
15
+ assert_equal "\"accounts\".\"last_name\"", search.order
43
16
 
44
17
  search.order_by = ["first_name", "last_name"]
45
18
  assert_equal ["first_name", "last_name"], search.order_by
46
- assert_equal "\"accounts\".\"first_name\" ASC, \"accounts\".\"last_name\" ASC", search.order
19
+ assert_equal "\"accounts\".\"first_name\", \"accounts\".\"last_name\"", search.order
47
20
 
48
21
  search.order = "created_at DESC"
49
22
  assert_equal "created_at", search.order_by
@@ -77,4 +50,98 @@ class TestSearchOrdering < Test::Unit::TestCase
77
50
  assert_equal nil, search.order_by
78
51
  assert_equal "`line_items`.id DESC", search.order
79
52
  end
53
+
54
+ def test_order_as
55
+ search = Account.new_search
56
+ assert_equal nil, search.order
57
+ assert_equal nil, search.order_as
58
+ assert !search.asc?
59
+
60
+ search.order_as = "DESC"
61
+ assert_equal nil, search.order_as
62
+ assert !search.desc?
63
+ assert_equal nil, search.order
64
+
65
+ search.order_by = "name"
66
+ assert_equal "\"accounts\".\"name\" DESC", search.order
67
+
68
+ search.order_as = "ASC"
69
+ assert_equal "\"accounts\".\"name\" ASC", search.order
70
+ assert search.asc?
71
+
72
+ search.order = "id ASC"
73
+ assert_equal "ASC", search.order_as
74
+ assert search.asc?
75
+ assert_equal "id ASC", search.order
76
+
77
+ search.order = "id DESC"
78
+ assert_equal "DESC", search.order_as
79
+ assert search.desc?
80
+ assert_equal "id DESC", search.order
81
+
82
+ search.order_by = "name"
83
+ assert_equal "DESC", search.order_as
84
+ assert search.desc?
85
+ assert_equal "\"accounts\".\"name\" DESC", search.order
86
+
87
+ assert_raise(ArgumentError) { search.order_as = "awesome" }
88
+ end
89
+
90
+ def test_order_by_auto_joins
91
+ search = Account.new_search
92
+ assert_equal nil, search.order_by_auto_joins
93
+ search.order_by = :name
94
+ assert_equal nil, search.order_by_auto_joins
95
+ search.order_by = {:users => :first_name}
96
+ assert_equal :users, search.order_by_auto_joins
97
+ search.order_by = [{:users => :first_name}, {:orders => :total}, {:users => {:user_groups => :name}}]
98
+ assert_equal [:users, :orders, {:users => :user_groups}], search.order_by_auto_joins
99
+ search.priority_order_by = {:users => :first_name}
100
+ assert_equal [:users, :orders, {:users => :user_groups}], search.order_by_auto_joins
101
+ search.priority_order_by = {:users => {:orders => :total}}
102
+ assert_equal({:users => :orders}, search.priority_order_by_auto_joins)
103
+ assert_equal [:users, :orders, {:users => :user_groups}, {:users => :orders}], search.auto_joins
104
+ end
105
+
106
+ def test_priority_order_by
107
+ search = Account.new_search
108
+ assert_equal nil, search.priority_order
109
+ assert_equal nil, search.priority_order_by
110
+ assert_equal nil, search.priority_order_as
111
+
112
+ search.priority_order_by = :name
113
+ assert_equal "\"accounts\".\"name\"", search.priority_order
114
+ assert_equal "\"accounts\".\"name\"", search.sanitize[:order]
115
+ assert_equal nil, search.order
116
+ assert_equal :name, search.priority_order_by
117
+ assert_equal nil, search.priority_order_as
118
+
119
+ search.order_by = :id
120
+ assert_equal "\"accounts\".\"name\", \"accounts\".\"id\"", search.sanitize[:order]
121
+ search.order_as = "DESC"
122
+ assert_equal "\"accounts\".\"name\", \"accounts\".\"id\" DESC", search.sanitize[:order]
123
+ end
124
+
125
+ def test_priority_order_as
126
+ search = Account.new_search
127
+ assert_equal nil, search.priority_order_as
128
+ assert_equal nil, search.order_as
129
+ search.priority_order_as = "ASC"
130
+ assert_equal nil, search.priority_order_as
131
+ assert_equal nil, search.order_as
132
+ search.priority_order_by = :name
133
+ assert_equal "ASC", search.priority_order_as
134
+ assert_equal nil, search.order_as
135
+ search.priority_order_as = "DESC"
136
+ assert_equal "DESC", search.priority_order_as
137
+ assert_equal nil, search.order_as
138
+ assert_raise(ArgumentError) { search.priority_order_as = "awesome" }
139
+ search.priority_order = nil
140
+ assert_equal nil, search.priority_order_as
141
+ assert_equal nil, search.order_as
142
+ end
143
+
144
+ def test_sanitize
145
+ # tested in test_priority_order_by
146
+ end
80
147
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchgasm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Johnson of Binary Logic
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-09-24 00:00:00 -04:00
12
+ date: 2008-09-26 00:00:00 -04:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -31,9 +31,6 @@ dependencies:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: "0"
34
- - - "="
35
- - !ruby/object:Gem::Version
36
- version: 2.1.0
37
34
  version:
38
35
  - !ruby/object:Gem::Dependency
39
36
  name: echoe