searchgasm 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
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