searchgasm 1.1.0 → 1.1.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,11 @@
1
+ == 1.1.1 released 2008-09-19
2
+
3
+ * Fixed typo in "next page" button.
4
+ * Updated valid options for searching and performing calculations, fixed some bugs when searching and performing calculations with advanced options.
5
+ * Fixed bug in ordering where table name was assumed by the hash. Now assumed by the reflection.
6
+ * Added default for per_page, so pagination comes implemented by default
7
+ * On mass assignments blank strings for *any* conditions are ignored. Sometimes blank strings are meaningful for "equals" and "does not equal", those only takes effect if you explicitly call these conditions: search.conditions.name = "". User.new_search(:conditions => {:name => ""}) will be ignored. Also, Searchgasm should never change how ActiveRecord behaves by default. So User.all(:conditions => {:name => ""}) will NOT be ignored.
8
+
1
9
  == 1.1.0 released 2008-09-18
2
10
 
3
11
  * Added the options :inner_spread and :outer_spread to the page_links helper. Also added various config options for setting page_links defaults.
@@ -8,7 +8,7 @@ module Searchgasm
8
8
  # This needs to be implemented because AR doesn't leverage scopes with this method like it probably should
9
9
  def find_with_searchgasm(*args)
10
10
  options = args.extract_options!
11
- args << sanitize_options_with_searchgasm(options)
11
+ args << filter_options_with_searchgasm(options)
12
12
  find_without_searchgasm(*args)
13
13
  end
14
14
 
@@ -36,7 +36,7 @@ module Searchgasm
36
36
  module Shared
37
37
  def count_with_searchgasm(*args)
38
38
  options = args.extract_options!
39
- args << sanitize_options_with_searchgasm(options)
39
+ args << filter_options_with_searchgasm(options)
40
40
  count_without_searchgasm(*args)
41
41
  end
42
42
  end
@@ -6,21 +6,15 @@ module Searchgasm
6
6
  # This is an alias method chain. It hook into ActiveRecord's "calculate" method and checks to see if Searchgasm should get involved.
7
7
  def calculate_with_searchgasm(*args)
8
8
  options = args.extract_options!
9
- options = sanitize_options_with_searchgasm(options)
9
+ options = filter_options_with_searchgasm(options)
10
10
  args << options
11
11
  calculate_without_searchgasm(*args)
12
12
  end
13
13
 
14
- def total(*args)
15
- options = args.extract_options!
16
- searcher = searchgasm_searcher(options)
17
- searcher.total
18
- end
19
-
20
14
  # This is an alias method chain. It hooks into ActiveRecord's "find" method and checks to see if Searchgasm should get involved.
21
15
  def find_with_searchgasm(*args)
22
16
  options = args.extract_options!
23
- options = sanitize_options_with_searchgasm(options)
17
+ options = filter_options_with_searchgasm(options)
24
18
  args << options
25
19
  find_without_searchgasm(*args)
26
20
  end
@@ -45,7 +39,7 @@ module Searchgasm
45
39
  # build_search
46
40
  # end
47
41
  def with_scope_with_searchgasm(method_scoping = {}, action = :merge, &block)
48
- method_scoping[:find] = sanitize_options_with_searchgasm(method_scoping[:find]) if method_scoping[:find]
42
+ method_scoping[:find] = filter_options_with_searchgasm(method_scoping[:find]) if method_scoping[:find]
49
43
  with_scope_without_searchgasm(method_scoping, action, &block)
50
44
  end
51
45
 
@@ -130,10 +124,19 @@ module Searchgasm
130
124
  end
131
125
 
132
126
  private
133
- def sanitize_options_with_searchgasm(options = {})
127
+ def filter_options_with_searchgasm(options = {})
134
128
  return options unless Searchgasm::Search::Base.needed?(self, options)
135
129
  search = Searchgasm::Search::Base.create_virtual_class(self).new # call explicitly to avoid merging the scopes into the searcher
136
130
  search.acting_as_filter = true
131
+ conditions = options.delete(:conditions) || options.delete("conditions") || {}
132
+ if conditions
133
+ case conditions
134
+ when Hash
135
+ conditions.each { |condition, value| search.conditions.send("#{condition}=", value) } # explicitly call to enforce blanks
136
+ else
137
+ search.conditions = conditions
138
+ end
139
+ end
137
140
  search.options = options
138
141
  search.sanitize
139
142
  end
@@ -181,8 +184,12 @@ module ActiveRecord #:nodoc: all
181
184
  alias_method :new_search!, :build_search!
182
185
 
183
186
  def valid_find_options
184
- VALID_FIND_OPTIONS
185
- end
187
+ VALID_FIND_OPTIONS
188
+ end
189
+
190
+ def valid_calculations_options
191
+ Calculations::CALCULATIONS_OPTIONS
192
+ end
186
193
  end
187
194
  end
188
195
  end
@@ -135,7 +135,10 @@ module Searchgasm
135
135
  case conditions
136
136
  when Hash
137
137
  assert_valid_conditions(conditions)
138
- remove_conditions_from_protected_assignement(conditions).each { |condition, value| send("#{condition}=", value) }
138
+ remove_conditions_from_protected_assignement(conditions).each do |condition, value|
139
+ next if value.blank? # ignore blanks on mass assignments
140
+ send("#{condition}=", value)
141
+ end
139
142
  else
140
143
  self.sql = conditions
141
144
  end
@@ -92,12 +92,12 @@ module Searchgasm
92
92
  end
93
93
 
94
94
  def page_links_next # :nodoc:
95
- @page_links_next ||= "< Next"
95
+ @page_links_next ||= "Next >"
96
96
  end
97
97
 
98
98
  # The default for the :next option for the page_links helper.
99
99
  #
100
- # * <tt>Default:</tt> "< Next"
100
+ # * <tt>Default:</tt> "Next >"
101
101
  # * <tt>Accepts:</tt> Anything you want, text, html, etc. nil to disable
102
102
  def page_links_next=(value)
103
103
  @page_links_next = value
@@ -116,14 +116,14 @@ module Searchgasm
116
116
  end
117
117
 
118
118
  def per_page # :nodoc:
119
- @per_page
119
+ @per_page ||= per_page_choices[2]
120
120
  end
121
121
 
122
122
  # The default for per page. This is only applicaple for protected searches. Meaning you start the search with new_search or new_conditions.
123
123
  # The reason for this not to disturb regular queries such as Whatever.find(:all). You would not expect that to be limited.
124
124
  #
125
- # * <tt>Default:</tt> nil, nil means "show all"
126
- # * <tt>Accepts:</tt> Any value in your per_page choices
125
+ # * <tt>Default:</tt> The 3rd option in your per_page_choices, default of 50
126
+ # * <tt>Accepts:</tt> Any value in your per_page choices, nil means "show all"
127
127
  def per_page=(value)
128
128
  @per_page = value
129
129
  end
@@ -9,13 +9,21 @@ module Searchgasm #:nodoc:
9
9
  include Searchgasm::Shared::Searching
10
10
  include Searchgasm::Shared::VirtualClasses
11
11
 
12
+ # Options ActiveRecord allows when searching
13
+ AR_FIND_OPTIONS = ::ActiveRecord::Base.valid_find_options
14
+
15
+ # Options ActiveRecord allows when performing calculations
16
+ AR_CALCULATIONS_OPTIONS = ::ActiveRecord::Base.valid_calculations_options
17
+
18
+ AR_OPTIONS = (AR_FIND_OPTIONS + AR_CALCULATIONS_OPTIONS).uniq
19
+
12
20
  # Options that ActiveRecord doesn't suppport, but Searchgasm does
13
21
  SPECIAL_FIND_OPTIONS = [:order_by, :order_as, :page, :per_page]
14
22
 
15
23
  # Valid options you can use when searching
16
- VALID_FIND_OPTIONS = SPECIAL_FIND_OPTIONS + ::ActiveRecord::Base.valid_find_options # the order is very important, these options get set in this order
24
+ OPTIONS = SPECIAL_FIND_OPTIONS + AR_OPTIONS # the order is very important, these options get set in this order
17
25
 
18
- attr_accessor *::ActiveRecord::Base.valid_find_options
26
+ attr_accessor *AR_OPTIONS
19
27
 
20
28
  class << self
21
29
  # Used in the ActiveRecord methods to determine if Searchgasm should get involved or not.
@@ -47,7 +55,7 @@ module Searchgasm #:nodoc:
47
55
  # Makes using searchgasm in the console less annoying and keeps the output meaningful and useful
48
56
  def inspect
49
57
  current_find_options = {}
50
- ::ActiveRecord::Base.valid_find_options.each do |option|
58
+ AR_OPTIONS.each do |option|
51
59
  value = send(option)
52
60
  next if value.nil?
53
61
  current_find_options[option] = value
@@ -72,10 +80,9 @@ module Searchgasm #:nodoc:
72
80
 
73
81
  def options=(values)
74
82
  return unless values.is_a?(Hash)
75
- values.symbolize_keys.fast_assert_valid_keys(VALID_FIND_OPTIONS)
83
+ values.symbolize_keys.fast_assert_valid_keys(OPTIONS)
76
84
 
77
- # Do the special options first, and then the core options last, since the core options take precendence
78
- VALID_FIND_OPTIONS.each do |option|
85
+ OPTIONS.each do |option|
79
86
  next unless values.has_key?(option)
80
87
  send("#{option}=", values[option])
81
88
  end
@@ -84,9 +91,9 @@ module Searchgasm #:nodoc:
84
91
  end
85
92
 
86
93
  # Sanitizes everything down into options ActiveRecord::Base.find can understand
87
- def sanitize
94
+ def sanitize(searching = true)
88
95
  find_options = {}
89
- ::ActiveRecord::Base.valid_find_options.each do |find_option|
96
+ (searching ? AR_FIND_OPTIONS : AR_CALCULATIONS_OPTIONS).each do |find_option|
90
97
  value = send(find_option)
91
98
  next if value.blank?
92
99
  find_options[find_option] = value
@@ -51,8 +51,8 @@ module Searchgasm
51
51
  includes.blank? ? nil : (includes.size == 1 ? includes.first : includes)
52
52
  end
53
53
 
54
- def sanitize_with_conditions # :nodoc:
55
- find_options = sanitize_without_conditions
54
+ def sanitize_with_conditions(searching = true) # :nodoc:
55
+ find_options = sanitize_without_conditions(searching)
56
56
  if conditions_obj = find_options.delete(:conditions)
57
57
  new_conditions = conditions_obj.sanitize
58
58
  find_options[:conditions] = new_conditions unless new_conditions.blank?
@@ -115,7 +115,8 @@ module Searchgasm
115
115
 
116
116
  private
117
117
  def order_by_to_order(order_by, order_as, alt_klass = nil, new_includes = [])
118
- table_name = (alt_klass || klass).table_name
118
+ k = alt_klass || klass
119
+ table_name = k.table_name
119
120
  sql_parts = []
120
121
 
121
122
  case order_by
@@ -123,10 +124,11 @@ module Searchgasm
123
124
  order_by.each { |part| sql_parts << order_by_to_order(part, order_as) }
124
125
  when Hash
125
126
  raise(ArgumentError, "when passing a hash to order_by you must only have 1 key: {:user_group => :name} not {:user_group => :name, :user_group => :id}. The latter should be [{:user_group => :name}, {:user_group => :id}]") if order_by.keys.size != 1
126
- k = order_by.keys.first
127
- v = order_by.values.first
128
- new_includes << k.to_sym
129
- sql_parts << order_by_to_order(v, order_as, eval(k.to_s.classify), new_includes) # using eval, better performance, protection makes sure nothing fishy goes on here
127
+ key = order_by.keys.first
128
+ reflection = k.reflect_on_association(key.to_sym)
129
+ value = order_by.values.first
130
+ new_includes << key.to_sym
131
+ sql_parts << order_by_to_order(value, order_as, reflection.klass, new_includes) # using eval, better performance, protection makes sure nothing fishy goes on here
130
132
  when Symbol, String
131
133
  new_include = build_order_by_includes(new_includes)
132
134
  self.order_by_includes << new_include if new_include
@@ -24,8 +24,12 @@ module Searchgasm
24
24
  # Options that are allowed when protecting against SQL injections (still checked though)
25
25
  SAFE_OPTIONS = Base::SPECIAL_FIND_OPTIONS + [:conditions, :limit, :offset]
26
26
 
27
+ VULNERABLE_FIND_OPTIONS = Base::AR_FIND_OPTIONS - SAFE_OPTIONS
28
+
29
+ VULNERABLE_CALCULATIONS_OPTIONS = Base::AR_CALCULATIONS_OPTIONS - SAFE_OPTIONS
30
+
27
31
  # Options that are not allowed, at all, when protecting against SQL injections
28
- VULNERABLE_OPTIONS = Base::VALID_FIND_OPTIONS - SAFE_OPTIONS
32
+ VULNERABLE_OPTIONS = Base::OPTIONS - SAFE_OPTIONS
29
33
 
30
34
  def self.included(klass)
31
35
  klass.class_eval do
@@ -59,12 +63,13 @@ module Searchgasm
59
63
 
60
64
  k = alt_klass || klass
61
65
  column_names = k.column_names
62
-
66
+
63
67
  [order_by].flatten.each do |column|
64
68
  case column
65
69
  when Hash
66
- return false unless k.reflect_on_association(column.keys.first.to_sym)
67
- return false unless order_by_safe?(column.values.first, column.keys.first.to_s.classify.constantize)
70
+ reflection = k.reflect_on_association(column.keys.first.to_sym)
71
+ return false unless reflection
72
+ return false unless order_by_safe?(column.values.first, reflection.klass)
68
73
  when Array
69
74
  return false unless order_by_safe?(column)
70
75
  else
@@ -21,17 +21,15 @@ module Searchgasm
21
21
  end
22
22
  end_eval
23
23
  end
24
-
24
+
25
25
  # Setup methods for calculating
26
26
  CALCULATION_METHODS.each do |method|
27
27
  class_eval <<-"end_eval", __FILE__, __LINE__
28
28
  def #{method}(*args)
29
29
  options = args.extract_options!
30
30
  klass.send(:with_scope, :find => options) do
31
- find_options = (self.class < Searchgasm::Conditions::Base ? {:conditions => sanitize} : sanitize)
32
- find_options.delete(:select)
33
- find_options.delete(:limit)
34
- find_options.delete(:offset)
31
+ find_options = (self.class < Searchgasm::Conditions::Base ? {:conditions => sanitize} : sanitize(false))
32
+ [:select, :limit, :offset].each { |option| find_options.delete(option) }
35
33
  args << find_options
36
34
  klass.#{method}(*args)
37
35
  end
@@ -67,7 +67,7 @@ module Searchgasm
67
67
 
68
68
  MAJOR = 1
69
69
  MINOR = 1
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.1.0
2
+ # Gem::Specification for Searchgasm-1.1.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.1.0
8
+ version: 1.1.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-18 00:00:00 -04:00
15
+ date: 2008-09-19 00:00:00 -04:00
16
16
  default_executable:
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
@@ -28,8 +28,9 @@ class TestActiveRecordBase < Test::Unit::TestCase
28
28
  assert_nothing_raised { Account.sum("id", {}) }
29
29
  end
30
30
 
31
- def test_valid_find_options
31
+ def test_ar_options
32
32
  assert_equal [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :from, :lock ], ActiveRecord::Base.valid_find_options
33
+ assert_equal [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include, :from], ActiveRecord::Base.valid_calculations_options
33
34
  end
34
35
 
35
36
  def test_build_search
@@ -185,4 +185,13 @@ class TestConditionsBase < Test::Unit::TestCase
185
185
  conditions.any = false
186
186
  assert_equal [Account.find(1)], conditions.all
187
187
  end
188
+
189
+ def test_ignoring_blanks
190
+ conditions = Account.new_conditions(:name => "", :created_after => nil)
191
+ assert_equal({}, conditions.conditions)
192
+ conditions.name = ""
193
+ assert_equal({:name_equals => ""}, conditions.conditions)
194
+ conditions.created_after = ""
195
+ assert_equal({:name_equals => ""}, conditions.conditions)
196
+ end
188
197
  end
@@ -20,5 +20,8 @@ class TestConditionsProtection < Test::Unit::TestCase
20
20
 
21
21
  assert_raise(ArgumentError) { account.users.build_conditions("(DELETE FROM users)") }
22
22
  assert_nothing_raised { account.users.build_conditions!("(DELETE FROM users)") }
23
+
24
+ #search = Account.new_search
25
+ #assert_nothing_raised { search.conditions = "(DELETE FROM users)" }
23
26
  end
24
27
  end
data/test/test_helper.rb CHANGED
@@ -9,6 +9,7 @@ require File.dirname(__FILE__) + '/../lib/searchgasm'
9
9
  ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
10
10
 
11
11
  class Account < ActiveRecord::Base
12
+ has_one :admin, :class_name => "User", :conditions => {:first_name => "Ben"}
12
13
  has_many :users, :dependent => :destroy
13
14
  has_many :orders, :through => :users
14
15
  end
@@ -17,6 +18,7 @@ class User < ActiveRecord::Base
17
18
  acts_as_tree
18
19
  belongs_to :account
19
20
  has_many :orders, :dependent => :destroy
21
+ has_one :cool_order, :class_name => "Order", :conditions => {:total => 100}
20
22
  end
21
23
 
22
24
  class Order < ActiveRecord::Base
@@ -39,7 +39,19 @@ class TestSearchBase < Test::Unit::TestCase
39
39
  end
40
40
 
41
41
  def test_setting_first_level_options
42
- search = Account.new_search
42
+ search = Account.new_search!(:include => :users, :joins => :test, :offset => 5, :limit => 20, :order => "name ASC", :select => "name", :readonly => true, :group => "name", :from => "accounts", :lock => true)
43
+ assert_equal :users, search.include
44
+ assert_equal :test, search.joins
45
+ assert_equal 5, search.offset
46
+ assert_equal 20, search.limit
47
+ assert_equal "name ASC", search.order
48
+ assert_equal "name", search.select
49
+ assert_equal true, search.readonly
50
+ assert_equal "name", search.group
51
+ assert_equal "accounts", search.from
52
+ assert_equal true, search.lock
53
+
54
+ search = Account.new_search(:per_page => nil)
43
55
 
44
56
  search.include = :users
45
57
  assert_equal :users, search.include
@@ -190,6 +202,9 @@ class TestSearchBase < Test::Unit::TestCase
190
202
  assert_equal 3, search.maximum('id')
191
203
  assert_equal 1, search.minimum('id')
192
204
  assert_equal 4, search.sum('id')
205
+
206
+ search.readonly = true
207
+ assert_equal 4, search.sum('id')
193
208
  end
194
209
 
195
210
  def test_method_creation_in_scope
@@ -29,7 +29,7 @@ class TestSearchPagination < Test::Unit::TestCase
29
29
  end
30
30
 
31
31
  def test_page
32
- search = Account.new_search
32
+ search = Account.new_search(:per_page => nil)
33
33
  search.page = 2
34
34
  assert_equal 1, search.page
35
35
  search.per_page = 20
@@ -14,18 +14,18 @@ class TestSearchProtection < Test::Unit::TestCase
14
14
 
15
15
  def test_protection
16
16
  assert_raise(ArgumentError) { Account.build_search(:conditions => "(DELETE FROM users)", :page => 2, :per_page => 15) }
17
- Searchgasm::Search::Base::VULNERABLE_OPTIONS.each { |option| assert_raise(ArgumentError) { Account.build_search(option => "(DELETE FROM users)") } }
17
+ Searchgasm::Search::Base::VULNERABLE_FIND_OPTIONS.each { |option| assert_raise(ArgumentError) { Account.build_search(option => "(DELETE FROM users)") } }
18
18
 
19
19
  assert_nothing_raised { Account.build_search!(:conditions => "(DELETE FROM users)", :page => 2, :per_page => 15) }
20
- Searchgasm::Search::Base::VULNERABLE_OPTIONS.each { |option| assert_nothing_raised { Account.build_search!(option => "(DELETE FROM users)") } }
20
+ Searchgasm::Search::Base::VULNERABLE_FIND_OPTIONS.each { |option| assert_nothing_raised { Account.build_search!(option => "(DELETE FROM users)") } }
21
21
 
22
22
  account = Account.first
23
23
 
24
24
  assert_raise(ArgumentError) { account.users.build_search(:conditions => "(DELETE FROM users)", :page => 2, :per_page => 15) }
25
- Searchgasm::Search::Base::VULNERABLE_OPTIONS.each { |option| assert_raise(ArgumentError) { account.users.build_search(option => "(DELETE FROM users)") } }
25
+ Searchgasm::Search::Base::VULNERABLE_FIND_OPTIONS.each { |option| assert_raise(ArgumentError) { account.users.build_search(option => "(DELETE FROM users)") } }
26
26
 
27
27
  assert_nothing_raised { account.users.build_search!(:conditions => "(DELETE FROM users)", :page => 2, :per_page => 15) }
28
- Searchgasm::Search::Base::VULNERABLE_OPTIONS.each { |option| assert_nothing_raised { account.users.build_search!(option => "(DELETE FROM users)") } }
28
+ Searchgasm::Search::Base::VULNERABLE_FIND_OPTIONS.each { |option| assert_nothing_raised { account.users.build_search!(option => "(DELETE FROM users)") } }
29
29
 
30
30
  assert_raise(ArgumentError) { Account.build_search(:order_by => "unknown_column") }
31
31
  assert_nothing_raised { Account.build_search!(:order_by => "unknown_column") }
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.1.0
4
+ version: 1.1.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-18 00:00:00 -04:00
12
+ date: 2008-09-19 00:00:00 -04:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency