searchgasm 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,64 @@
1
+ == 1.0.1 released 2008-09-08
2
+
3
+ * Cached "searchers" so when a new search object is instantiated it doesn't go through all of the meta programming and method creation. Helps a lot with performance. You will see the speed benefits after the first instantiation.
4
+ * Added in new options for page_links.
5
+ * Fixed minor bugs when doing page_links.
6
+ * Updated documentation to be more detailed and inclusive.
7
+
8
+ == 1.0.0 released 2008-09-08
9
+
10
+ * Major changes in the helpers, they were completely re-engineered. Hence the new version. I established a pattern between all helpers giving you complete flexibility as to how they are used. All helpers are called differently now (see documentation).
11
+
12
+ == 0.9.10 released 2008-09-08
13
+
14
+ * Fixed bug with setting the per_page configuration to only take effect on protected searches, thus staying out of the way of normal searching.
15
+ * Hardened more tests
16
+
17
+ == 0.9.9 released 2008-09-07
18
+
19
+ * Fixed setting per_page to nil, false, or ''. This is done to "show all" results.
20
+
21
+ == 0.9.8 released 2008-09-06
22
+
23
+ * Fixed order_by helper bug when guessing the text with arrays. Should use the first value instead of last.
24
+ * Added in per_page config option.
25
+
26
+ == 0.9.7 released 2008-09-06
27
+
28
+ * Complete class restructure. Moved the 3 main components into their own base level class: Search, Conditions, Condition
29
+ * Split logic and functionality into their own modules, implemented via alias_chain_method
30
+ * Added in helpers for using in a rails app
31
+ * Added link to documentation and live example in README
32
+ * Various small bug fixes
33
+ * Hardened tests
34
+
35
+ == 0.9.6 released 2008-09-04
36
+
37
+ * Fixed bug when instantiating with nil options
38
+
39
+ == 0.9.5 released 2008-09-03
40
+
41
+ * Enhanced searching with conditions only, added in search methods and calculations
42
+ * Updated README to include examples
43
+
44
+ == 0.9.4 released 2008-09-03
45
+
46
+ * Cleaned up search methods
47
+ * Removed reset!method for both searching and searching by conditions
48
+
49
+ == 0.9.3 released 2008-09-02
50
+
51
+ * Changed structure of conditions to have their own class
52
+ * Added API for adding your own conditions.
53
+
54
+ == 0.9.2 released 2008-09-02
55
+
56
+ * Enhanced protection from SQL injections (made more efficient)
57
+
58
+ == 0.9.1 released 2008-09-02
59
+
60
+ * Added aliases for datetime, date, time, and timestamp attrs. You could call created_at_after, now you can also call created_after. Just removed the "at" requirement.
61
+
62
+ == 0.9.0 released 2008-09-01
63
+
64
+ * First release
data/Manifest CHANGED
@@ -1,8 +1,9 @@
1
- CHANGELOG
1
+ CHANGELOG.rdoc
2
2
  examples/README.rdoc
3
3
  init.rb
4
4
  lib/searchgasm/active_record/associations.rb
5
5
  lib/searchgasm/active_record/base.rb
6
+ lib/searchgasm/active_record.rb
6
7
  lib/searchgasm/condition/base.rb
7
8
  lib/searchgasm/condition/begins_with.rb
8
9
  lib/searchgasm/condition/child_of.rb
@@ -22,6 +23,7 @@ lib/searchgasm/condition/tree.rb
22
23
  lib/searchgasm/conditions/base.rb
23
24
  lib/searchgasm/conditions/protection.rb
24
25
  lib/searchgasm/config.rb
26
+ lib/searchgasm/core_ext/hash.rb
25
27
  lib/searchgasm/helpers/control_types/link.rb
26
28
  lib/searchgasm/helpers/control_types/links.rb
27
29
  lib/searchgasm/helpers/control_types/remote_link.rb
data/README.rdoc CHANGED
@@ -8,7 +8,7 @@ Searchgasm is orgasmic. Maybe not orgasmic, but you will get aroused. So go grab
8
8
 
9
9
  * <b>Documentation:</b> http://searchgasm.rubyforge.org
10
10
  * <b>Easy pagination, ordering, and searching tutorial:</b> http://www.binarylogic.com/2008/9/7/tutorial-pagination-ordering-and-searching-with-searchgasm
11
- * <b>The tutorial above, live:</b> http://searchgasm_example.binarylogic.com
11
+ * <b>Live example of the tutorial above (with source):</b> http://searchgasm_example.binarylogic.com
12
12
 
13
13
  == Install and use
14
14
 
@@ -67,8 +67,8 @@ Now your view. Things to note in this view:
67
67
 
68
68
  1. Passing a search object right into form\_for and fields\_for
69
69
  2. The built in conditions for each column and how you can traverse the relationships and set conditions on them
70
- 3. The order_by helper
71
- 4. The page and per_page helpers
70
+ 3. The order_by_link helper
71
+ 4. The page_select and per_page_select helpers
72
72
  5. All of your search logic is in 1 spot: your view. Nice and DRY.
73
73
 
74
74
  Your view:
@@ -82,25 +82,28 @@ Your view:
82
82
  = orders.select :total_gt, (1..100)
83
83
  = f.submit "Search"
84
84
 
85
- %table
86
- %tr
87
- %th= order_by_link :account => :name
88
- %th= order_by_link :first_name
89
- %th= order_by_link :last_name
90
- %th= order_by_link :email
91
- - @users.each do |user|
85
+ - if @users_count > 0
86
+ %table
87
+ %tr
88
+ %th= order_by_link :account => :name
89
+ %th= order_by_link :first_name
90
+ %th= order_by_link :last_name
91
+ %th= order_by_link :email
92
+ - @users.each do |user|
92
93
  %tr
93
94
  %td= user.account? ? user.account.name : "-"
94
95
  %td= user.first_name
95
96
  %td= user.last_name
96
97
  %td= user.email
97
98
 
98
- Per page:
99
- = per_page_select
100
- Page:
101
- = page_select
99
+ Per page:
100
+ = per_page_select
101
+ Page:
102
+ = page_select
103
+ - else
104
+ No users were found.
102
105
 
103
- <b>{See my tutorial on this example}(http://www.binarylogic.com/2008/9/7/tutorial-pagination-ordering-and-searching-with-searchgasm)</b>
106
+ <b>See my tutorial on this example: http://www.binarylogic.com/2008/9/7/tutorial-pagination-ordering-and-searching-with-searchgasm</b>
104
107
 
105
108
  == Exhaustive Example w/ Object Based Searching (great for form_for or fields_for)
106
109
 
@@ -174,13 +177,6 @@ Any of the options used in the above example can be used in these, but for the s
174
177
  search.per_page = 20
175
178
  search.all
176
179
 
177
- If you want to use Searchgasm directly:
178
-
179
- search = Searchgasm::Search::Base.new(User, :conditions => {:age_gt => 18})
180
- search.conditions.first_name_contains = "Ben"
181
- search.per_page = 20
182
- search.all
183
-
184
180
  == Search with conditions only
185
181
 
186
182
  Don't need pagination, ordering, or any of the other options? Search with conditions only.
@@ -194,19 +190,13 @@ Pass a conditions object right into ActiveRecord:
194
190
 
195
191
  User.all(:conditions => conditions)
196
192
 
197
- Again, if you want to use Searchgasm directly:
198
-
199
- conditions = Searchgasm::Conditions::Base.new(User, :age_gt => 18)
200
- conditions.first_name_contains = "Ben"
201
- conditions.all
202
-
203
193
  == Scoped searching
204
194
 
205
195
  @current_user.orders.find(:all, :conditions => {:total_lte => 500})
206
196
  @current_user.orders.count(:conditions => {:total_lte => 500})
207
197
  @current_user.orders.sum('total', :conditions => {:total_lte => 500})
208
198
 
209
- search = @current_user.orders.build_search('total', :conditions => {:total_lte => 500})
199
+ search = @current_user.orders.build_search(:conditions => {:total_lte => 500})
210
200
 
211
201
  == Searching trees
212
202
 
@@ -1,31 +1,39 @@
1
1
  module Searchgasm
2
2
  module ActiveRecord
3
+ # = Searchgasm ActiveRecord Associations
4
+ #
5
+ # These methods hook into ActiveRecords association methods and add in searchgasm functionality.
3
6
  module Associations
4
7
  module AssociationCollection
8
+ # This is an alias method chain. It hook into ActiveRecord's "find" method for associations and checks to see if Searchgasm should get involved.
5
9
  def find_with_searchgasm(*args)
6
10
  options = args.extract_options!
7
11
  args << sanitize_options_with_searchgasm(options)
8
12
  find_without_searchgasm(*args)
9
13
  end
10
14
 
15
+ # See build_conditions under Searchgasm::ActiveRecord::Base. This is the same thing but for associations.
11
16
  def build_conditions(options = {}, &block)
12
17
  conditions = @reflection.klass.build_conditions(options, &block)
13
18
  conditions.scope = scope(:find)[:conditions]
14
19
  conditions
15
20
  end
16
21
 
22
+ # See build_conditions! under Searchgasm::ActiveRecord::Base. This is the same thing but for associations.
17
23
  def build_conditions!(options = {}, &block)
18
24
  conditions = @reflection.klass.build_conditions!(options, &block)
19
25
  conditions.scope = scope(:find)[:conditions]
20
26
  conditions
21
27
  end
22
-
28
+
29
+ # See build_search under Searchgasm::ActiveRecord::Base. This is the same thing but for associations.
23
30
  def build_search(options = {}, &block)
24
31
  conditions = @reflection.klass.build_search(options, &block)
25
32
  conditions.scope = scope(:find)[:conditions]
26
33
  conditions
27
34
  end
28
35
 
36
+ # See build_conditions! under Searchgasm::ActiveRecord::Base. This is the same thing but for associations.
29
37
  def build_search!(options = {}, &block)
30
38
  conditions = @reflection.klass.build_search!(options, &block)
31
39
  conditions.scope = scope(:find)[:conditions]
@@ -34,6 +42,7 @@ module Searchgasm
34
42
  end
35
43
 
36
44
  module HasManyAssociation
45
+ # This is an alias method chain. It hook into ActiveRecord's "calculate" method for has many associations and checks to see if Searchgasm should get involved.
37
46
  def count_with_searchgasm(*args)
38
47
  column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
39
48
  count_without_searchgasm(column_name, sanitize_options_with_searchgasm(options))
@@ -1,13 +1,17 @@
1
1
  module Searchgasm
2
- module ActiveRecord #:nodoc: all
2
+ module ActiveRecord
3
+ # = Searchgasm ActiveRecord Base
4
+ # Adds in base level functionality to ActiveRecord
3
5
  module Base
6
+ # This is an alias method chain. It hook into ActiveRecord's "calculate" method and checks to see if Searchgasm should get involved.
4
7
  def calculate_with_searchgasm(*args)
5
8
  options = args.extract_options!
6
9
  options = sanitize_options_with_searchgasm(options)
7
10
  args << options
8
11
  calculate_without_searchgasm(*args)
9
12
  end
10
-
13
+
14
+ # This is an alias method chain. It hooks into ActiveRecord's "find" method and checks to see if Searchgasm should get involved.
11
15
  def find_with_searchgasm(*args)
12
16
  options = args.extract_options!
13
17
  options = sanitize_options_with_searchgasm(options)
@@ -15,12 +19,31 @@ module Searchgasm
15
19
  find_without_searchgasm(*args)
16
20
  end
17
21
 
22
+ # This is an alias method chain. It hooks into ActiveRecord's scopes and checks to see if Searchgasm should get involved. Allowing you to use all of Searchgasms conditions and tools
23
+ # in scopes as well.
24
+ #
25
+ # === Examples
26
+ #
27
+ # named_scope :top_expensive, :conditions => {:total_gt => 1_000_000}, :per_page => 10
28
+ #
29
+ # with_scope(:find => {:conditions => {:total_gt => 1_000_000}, :per_page => 10}) do
30
+ # find(:all)
31
+ # end
18
32
  def scope_with_searchgasm(method, key = nil)
19
33
  scope = scope_without_searchgasm(method, key)
20
34
  return sanitize_options_with_searchgasm(scope) if key.nil? && method == :find && !scope.blank?
21
35
  scope
22
36
  end
23
-
37
+
38
+ # This is a special method that Searchgasm adds in. It returns a new conditions object on the model. So you can search by conditions *only*.
39
+ #
40
+ # <b>This method is "protected". Meaning it checks the passed options for SQL injections. So trying to write raw SQL in *any* of the option will result in a raised exception. It's safe to pass a params object when instantiating.</b>
41
+ #
42
+ # === Examples
43
+ #
44
+ # conditions = User.new_conditions
45
+ # conditions.first_name_contains = "Ben"
46
+ # conditions.all # can call any search method: first, find(:all), find(:first), sum("id"), etc...
24
47
  def build_conditions(values = {}, &block)
25
48
  conditions = searchgasm_conditions
26
49
  conditions.protect = true
@@ -28,13 +51,28 @@ module Searchgasm
28
51
  yield conditions if block_given?
29
52
  conditions
30
53
  end
31
-
54
+
55
+ # See build_conditions. This is the same method but *without* protection. Do *NOT* pass in a params object to this method.
32
56
  def build_conditions!(values = {}, &block)
33
57
  conditions = searchgasm_conditions(values)
34
58
  yield conditions if block_given?
35
59
  conditions
36
60
  end
37
-
61
+
62
+ # This is a special method that Searchgasm adds in. It returns a new search object on the model. So you can search via an object.
63
+ #
64
+ # <b>This method is "protected". Meaning it checks the passed options for SQL injections. So trying to write raw SQL in *any* of the option will result in a raised exception. It's safe to pass a params object when instantiating.</b>
65
+ #
66
+ # This method has an alias "new_search"
67
+ #
68
+ # === Examples
69
+ #
70
+ # search = User.new_search
71
+ # search.conditions.first_name_contains = "Ben"
72
+ # search.per_page = 20
73
+ # search.page = 2
74
+ # search.order_by = {:user_group => :name}
75
+ # search.all # can call any search method: first, find(:all), find(:first), sum("id"), etc...
38
76
  def build_search(options = {}, &block)
39
77
  search = searchgasm_searcher
40
78
  search.protect = true
@@ -42,26 +80,38 @@ module Searchgasm
42
80
  yield search if block_given?
43
81
  search
44
82
  end
45
-
83
+
84
+ # See build_search. This is the same method but *without* protection. Do *NOT* pass in a params object to this method.
85
+ #
86
+ # This also has an alias "new_search!"
46
87
  def build_search!(options = {}, &block)
47
88
  search = searchgasm_searcher(options)
48
89
  yield search if block_given?
49
90
  search
50
91
  end
51
92
 
93
+ # Similar to ActiveRecord's attr_protected, but for conditions. It will block any conditions in this array that are being mass assigned. Mass assignments are:
94
+ #
95
+ # === Examples
96
+ #
97
+ # search = User.new_search(:conditions => {:first_name_like => "Ben", :email_contains => "binarylogic.com"})
98
+ # search.options = {:conditions => {:first_name_like => "Ben", :email_contains => "binarylogic.com"}}
99
+ #
100
+ # If first_name_like is in the list of conditions_protected then it will be removed from the hash.
52
101
  def conditions_protected(*conditions)
53
102
  write_inheritable_attribute(:conditions_protected, Set.new(conditions.map(&:to_s)) + (protected_conditions || []))
54
103
  end
55
104
 
56
- def protected_conditions
105
+ def protected_conditions # :nodoc:
57
106
  read_inheritable_attribute(:conditions_protected)
58
107
  end
59
108
 
109
+ # This is the reverse of conditions_protected. You can specify conditions here and *only* these conditions will be allowed in mass assignment. Any condition not specified here will be blocked.
60
110
  def conditions_accessible(*conditions)
61
111
  write_inheritable_attribute(:conditions_accessible, Set.new(conditions.map(&:to_s)) + (protected_conditions || []))
62
112
  end
63
113
 
64
- def accessible_conditions
114
+ def accessible_conditions # :nodoc:
65
115
  read_inheritable_attribute(:conditions_accessible)
66
116
  end
67
117
 
@@ -72,11 +122,11 @@ module Searchgasm
72
122
  end
73
123
 
74
124
  def searchgasm_conditions(options = {})
75
- Searchgasm::Conditions::Base.new(self, options)
125
+ Searchgasm::Conditions::Base.create_virtual_class(self).new(options)
76
126
  end
77
127
 
78
128
  def searchgasm_searcher(options = {})
79
- Searchgasm::Search::Base.new(self, options)
129
+ Searchgasm::Search::Base.create_virtual_class(self).new(options)
80
130
  end
81
131
  end
82
132
  end
@@ -0,0 +1,8 @@
1
+ module Searchgasm
2
+ # == Searchgasm ActiveRecord
3
+ #
4
+ # Hooks into ActiveRecord to add all of the searchgasm functionality into your models. Only uses what is publically available, doesn't dig into internals, and
5
+ # searchgasm only gets involved when needed.
6
+ module ActiveRecord
7
+ end
8
+ end
@@ -7,9 +7,11 @@ module Searchgasm
7
7
  class Base
8
8
  include Utilities
9
9
 
10
- attr_accessor :klass, :relationship_name, :scope
10
+ attr_accessor :relationship_name, :scope
11
11
 
12
12
  class << self
13
+ attr_accessor :added_klass_conditions, :added_column_conditions, :added_associations
14
+
13
15
  # Registers a condition as an available condition for a column or a class.
14
16
  #
15
17
  # === Example
@@ -40,9 +42,9 @@ module Searchgasm
40
42
  # end
41
43
  #
42
44
  # Searchgasm::Seearch::Conditions.register_condition(SoundsLikeCondition)
43
- def register_condition(klass)
44
- raise(ArgumentError, "You can only register conditions that extend Searchgasm::Condition::Base") unless klass.ancestors.include?(Searchgasm::Condition::Base)
45
- conditions << klass unless conditions.include?(klass)
45
+ def register_condition(condition_class)
46
+ raise(ArgumentError, "You can only register conditions that extend Searchgasm::Condition::Base") unless condition_class.ancestors.include?(Searchgasm::Condition::Base)
47
+ conditions << condition_class unless conditions.include?(condition_class)
46
48
  end
47
49
 
48
50
  # A list of available condition type classes
@@ -50,19 +52,50 @@ module Searchgasm
50
52
  @@conditions ||= []
51
53
  end
52
54
 
53
- def needed?(klass, conditions) # :nodoc:
55
+ # A list of all associations created, used for caching and performance
56
+ def association_names
57
+ @association_names ||= []
58
+ end
59
+
60
+ # A list of all conditions available, users for caching and performance
61
+ def condition_names
62
+ @condition_names ||= []
63
+ end
64
+
65
+ def needed?(model_class, conditions) # :nodoc:
54
66
  if conditions.is_a?(Hash)
55
67
  conditions.stringify_keys.keys.each do |condition|
56
- return true unless klass.column_names.include?(condition)
68
+ return true unless model_class.column_names.include?(condition)
57
69
  end
58
70
  end
59
71
 
60
72
  false
61
73
  end
74
+
75
+ # Creates virtual classes for the class passed to it. This is a neccesity for keeping dynamically created method
76
+ # names specific to models. It provides caching and helps a lot with performance.
77
+ def create_virtual_class(model_class)
78
+ class_search_name = "::#{model_class.name}Conditions"
79
+
80
+ begin
81
+ class_search_name.constantize
82
+ rescue NameError
83
+ eval <<-end_eval
84
+ class #{class_search_name} < ::Searchgasm::Conditions::Base; end;
85
+ end_eval
86
+
87
+ class_search_name.constantize
88
+ end
89
+ end
90
+
91
+ # The class / model we are searching
92
+ def klass
93
+ # Can't cache this because thin and mongrel don't play nice with caching constants
94
+ name.split("::").last.gsub(/Conditions$/, "").constantize
95
+ end
62
96
  end
63
97
 
64
- def initialize(klass, init_conditions = {})
65
- self.klass = klass
98
+ def initialize(init_conditions = {})
66
99
  add_klass_conditions!
67
100
  add_column_conditions!
68
101
  add_associations!
@@ -90,6 +123,10 @@ module Searchgasm
90
123
  i.blank? ? nil : (i.size == 1 ? i.first : i)
91
124
  end
92
125
 
126
+ def klass
127
+ self.class.klass
128
+ end
129
+
93
130
  # Sanitizes the conditions down into conditions that ActiveRecord::Base.find can understand.
94
131
  def sanitize
95
132
  conditions = merge_conditions(*objects.collect { |object| object.sanitize })
@@ -128,11 +165,15 @@ module Searchgasm
128
165
 
129
166
  private
130
167
  def add_associations!
168
+ return true if self.class.added_associations
169
+
131
170
  klass.reflect_on_all_associations.each do |association|
171
+ self.class.association_names << association.name.to_s
172
+
132
173
  self.class.class_eval <<-"end_eval", __FILE__, __LINE__
133
174
  def #{association.name}
134
175
  if @#{association.name}.nil?
135
- @#{association.name} = self.class.new(#{association.class_name})
176
+ @#{association.name} = #{association.class_name}.new_conditions
136
177
  @#{association.name}.relationship_name = "#{association.name}"
137
178
  objects << @#{association.name}
138
179
  end
@@ -143,9 +184,13 @@ module Searchgasm
143
184
  def reset_#{association.name}!; objects.delete(#{association.name}); @#{association.name} = nil; end
144
185
  end_eval
145
186
  end
187
+
188
+ self.class.added_associations = true
146
189
  end
147
190
 
148
191
  def add_column_conditions!
192
+ return true if self.class.added_column_conditions
193
+
149
194
  klass.columns.each do |column|
150
195
  self.class.conditions.each do |condition_klass|
151
196
  name = condition_klass.name_for_column(column)
@@ -154,10 +199,13 @@ module Searchgasm
154
199
  condition_klass.aliases_for_column(column).each { |alias_name| add_condition_alias!(alias_name, name) }
155
200
  end
156
201
  end
202
+
203
+ self.class.added_column_conditions = true
157
204
  end
158
205
 
159
206
  def add_condition!(condition, name, column = nil)
160
- condition_names << name
207
+ self.class.condition_names << name
208
+
161
209
  self.class.class_eval <<-"end_eval", __FILE__, __LINE__
162
210
  def #{name}_object
163
211
  if @#{name}.nil?
@@ -174,7 +222,8 @@ module Searchgasm
174
222
  end
175
223
 
176
224
  def add_condition_alias!(alias_name, name)
177
- condition_names << alias_name
225
+ self.class.condition_names << alias_name
226
+
178
227
  self.class.class_eval do
179
228
  alias_method alias_name, name
180
229
  alias_method "#{alias_name}=", "#{name}="
@@ -182,26 +231,24 @@ module Searchgasm
182
231
  end
183
232
 
184
233
  def add_klass_conditions!
234
+ return true if self.class.added_klass_conditions
235
+
185
236
  self.class.conditions.each do |condition|
186
237
  name = condition.name_for_klass(klass)
187
238
  next if name.blank?
188
239
  add_condition!(condition, name)
189
240
  condition.aliases_for_klass(klass).each { |alias_name| add_condition_alias!(alias_name, name) }
190
241
  end
242
+
243
+ self.class.added_klass_conditions = true
191
244
  end
192
245
 
193
246
  def assert_valid_conditions(conditions)
194
- keys = condition_names.collect { |condition_name| condition_name.to_sym }
195
- keys += klass.reflect_on_all_associations.collect { |association| association.name }
196
- conditions.symbolize_keys.assert_valid_keys(keys)
247
+ conditions.stringify_keys.assert_valid_keys(self.class.condition_names + self.class.association_names)
197
248
  end
198
249
 
199
250
  def associations
200
- objects.select { |object| object.is_a?(self.class) }
201
- end
202
-
203
- def condition_names
204
- @condition_names ||= []
251
+ objects.select { |object| object.class < ::Searchgasm::Conditions::Base }
205
252
  end
206
253
 
207
254
  def objects
@@ -0,0 +1,22 @@
1
+ module Searchgasm
2
+ module CoreExt # :nodoc: all
3
+ module Hash
4
+ def deep_dup
5
+ new_hash = {}
6
+
7
+ self.each do |k, v|
8
+ case v
9
+ when Hash
10
+ new_hash[k] = v.deep_dup
11
+ else
12
+ new_hash[k] = v
13
+ end
14
+ end
15
+
16
+ new_hash
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ Hash.send(:include, Searchgasm::CoreExt::Hash)