searchgasm 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1 @@
1
+ v0.9.0. First release
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Ben Johnson of Binary Logic (binarylogic.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest ADDED
@@ -0,0 +1,28 @@
1
+ CHANGELOG
2
+ init.rb
3
+ lib/searchgasm/active_record/associations.rb
4
+ lib/searchgasm/active_record/base.rb
5
+ lib/searchgasm/active_record/protection.rb
6
+ lib/searchgasm/helpers.rb
7
+ lib/searchgasm/search/base.rb
8
+ lib/searchgasm/search/condition.rb
9
+ lib/searchgasm/search/conditions.rb
10
+ lib/searchgasm/search/utilities.rb
11
+ lib/searchgasm/version.rb
12
+ lib/searchgasm.rb
13
+ Manifest
14
+ MIT-LICENSE
15
+ Rakefile
16
+ README.mdown
17
+ test/fixtures/accounts.yml
18
+ test/fixtures/orders.yml
19
+ test/fixtures/users.yml
20
+ test/libs/acts_as_tree.rb
21
+ test/libs/rexml_fix.rb
22
+ test/test_active_record_associations.rb
23
+ test/test_active_record_base.rb
24
+ test/test_active_record_protection.rb
25
+ test/test_helper.rb
26
+ test/test_searchgasm_base.rb
27
+ test/test_searchgasm_condition.rb
28
+ test/test_searchgasm_conditions.rb
data/README.mdown ADDED
@@ -0,0 +1,240 @@
1
+ # Searchgasm
2
+
3
+ Searchgasm is orgasmic. Maybe not orgasmic, but you will get aroused. So go grab a towel and let's dive in.
4
+
5
+ Searchgasm originated to satisfy a VERY simple need: so that I could use my form builder when making search forms. Sounds simple right? The goal was to use an object, that represents a search, just like an ActiveRecord object in form\_for and fields\_for.
6
+
7
+ I'm a big fan of understanding what I'm using, so here's a quick explanation: The design behind this plugin is pretty simple. The search object "santiizes" down into the options passed into ActiveRecord::Base.find(). It basically serves as a transparent filter between you and ActiveRecord::Base.find(). This filter provides "enhancements" that get translated into options that ActiveRecord::Base.find() can understand. This doesn't step on the toes or dig into he internals of ActiveRecord. It uses what ActiveRecord provides publicly. Letting ActiveRecord do all of the hard work and keeping this plugin solid and less brittle.
8
+
9
+ Here's where you get aroused...
10
+
11
+ ## Install and use
12
+
13
+ sudo gem install searchgasm
14
+
15
+ For rails > 2.1
16
+
17
+ # environment.rb
18
+ config.gem "searchgasm"
19
+
20
+ For rails < 2.1
21
+
22
+ # environment.rb
23
+ require "searchgasm"
24
+
25
+ Or as a plugin
26
+
27
+ script/plugin install git://github.com/binarylogic/searchgasm.git
28
+
29
+ Now go into your console and try out any of these example with your own models.
30
+
31
+ **For all examples, let's assume the following relationships: User => Orders => Line Items**
32
+
33
+ ## Super Simple Example
34
+
35
+ User.all(
36
+ :conditions => {
37
+ :first_name_contains => "Ben",
38
+ :email_ends_with => "binarylogic.com"
39
+ },
40
+ :page => 3,
41
+ :per_page => 20
42
+ )
43
+
44
+ ## Detailed Example w/ object based searching
45
+
46
+ # new_search returns an object, you can call "find", "all", "first" also, see "different ways to search" below
47
+
48
+ search = User.new_search(
49
+ :conditions => {
50
+ :first_name_contains => "Ben",
51
+ :age_gt => 18,
52
+ :orders => {:total_lt => 100}
53
+ },
54
+ :per_page => 20,
55
+ :page => 2,
56
+ :order_by => {:orders => :total},
57
+ :order_as => "DESC"
58
+ )
59
+ search.conditions.email_ends_with = "binarylogic.com"
60
+ search.conditions.oders.line_items.created_at_after = Time.now
61
+ search.per_page = 50 # overrides the 20 set above
62
+
63
+ # Call ANY of the ActiveRecord options
64
+ search.group = "last_name"
65
+ search.readonly = true
66
+ # ... see ActiveRecord documentation
67
+
68
+ # Return results just like ActiveRecord
69
+ search.all
70
+ search.search # alias for all
71
+ search.first
72
+
73
+ ## Calculations
74
+
75
+ Using the object from above:
76
+
77
+ search.average('id')
78
+ search.count
79
+ search.maximum('id')
80
+ search.minimum('id')
81
+ search.sum('id')
82
+ search.calculate(:sum, 'id') # any of the above calculations
83
+
84
+ Or do it from your model:
85
+
86
+ User.count(:conditions => {:first_name_contains => "Ben"})
87
+ # ... all other calcualtions, etc.
88
+
89
+ ## Different ways to search, take your pick
90
+
91
+ Any of the options used in the above example can be used in these, but for the sake of brevity I am only using a few:
92
+
93
+ User.all(:conditions => {:age_gt => 18}, :per_page => 20)
94
+
95
+ User.first(:conditions => {:age_gt => 18}, :per_page => 20)
96
+
97
+ User.find(:all, :conditions => {::age_gt => 18}, :per_page => 20)
98
+
99
+ User.find(:first, :conditions => {::age_gt => 18}, :per_page => 20)
100
+
101
+ search = User.new_search(:conditions => {:age_gt => 18})
102
+ search.conditions.first_name_contains = "Ben"
103
+ search.per_page = 20
104
+ search.all
105
+
106
+ If you want to be hardcore:
107
+
108
+ search = Searchgasm::Search.new(User, :conditions => {:age_gt => 18})
109
+ search.conditions.first_name_contains = "Ben"
110
+ search.per_page = 20
111
+ search.all
112
+
113
+ ## Search with conditions only (great for form\_for or fields\_for)
114
+
115
+ conditions = User.new_conditions(:age_gt => 18)
116
+ conditions.first_name_contains = "Ben"
117
+ conditions.search
118
+ conditions.all
119
+ # ... all operations above are available
120
+
121
+ Pass a conditions object right into ActiveRecord:
122
+
123
+ User.all(:conditions => conditions) # same as conditions.search
124
+
125
+ Again, if you want to be hardcore:
126
+
127
+ conditions = Searchgasm::Conditions.new(User, :age_gt => 18)
128
+ conditions.first_name_contains = "Ben"
129
+ conditions.search
130
+
131
+ ## Scoped searching
132
+
133
+ @current_user.orders.find(:all, :conditions => {:total_lte => 500})
134
+ @current_user.orders.count(:conditions => {:total_lte => 500})
135
+ @current_user.orders.sum('total', :conditions => {:total_lte => 500})
136
+
137
+ search = @current_user.orders.build_search('total', :conditions => {:total_lte => 500})
138
+
139
+ ## Searching trees
140
+
141
+ For tree data structures you get a few nifty methods. Let's assume Users is a tree data structure.
142
+
143
+ # Child of
144
+ User.all(:conditions => {:child_of => User.roots.first})
145
+ User.all(:conditions => {:child_of => User.roots.first.id})
146
+
147
+ # Sibling of
148
+ User.all(:conditions => {:sibling_of => User.roots.first})
149
+ User.all(:conditions => {:sibling_of => User.roots.first.id})
150
+
151
+ # Descendent of (includes all recursive children: children, grand children, great grand children, etc)
152
+ User.all(:conditions => {:descendent_of => User.roots.first})
153
+ User.all(:conditions => {:descendent_of => User.roots.first.id})
154
+
155
+ # Inclusive descendent_of. Same as above but includes the root
156
+ User.all(:conditions => {:inclusive_descendent_of => User.roots.first})
157
+ User.all(:conditions => {:inclusive_descendent_of => User.roots.first.id})
158
+
159
+
160
+
161
+ ## Available anywhere (relationships & named scopes)
162
+
163
+ Not only can you use searchgasm when searching, but you can use it when setting up relationships or named scopes:
164
+
165
+ class User < ActiveRecord::Base
166
+ has_many :expensive_pending_orders, :conditions => {:total_greater_than => 1_000_000, :state => :pending}, :per_page => 20
167
+ named_scope :sexy, :conditions => {:first_name => "Ben", email_ends_with => "binarylogic.com"}, :per_page => 20
168
+ end
169
+
170
+ ## Always use protection (searching with params)
171
+
172
+ If there is one thing we in sex ed. it's to always use protection. As I mentioned above the purpose of this plugin was to create a search object and use it in form\_for or fields\_for. What about receiving the params in the controller and protecting against SQL injection?
173
+
174
+ accounts = Account.find_with_protection(params[:search])
175
+ accounts = Account.all_with_protection(params[:search])
176
+ account = Account.first_with_protection(params[:search])
177
+
178
+ For the lazy programmer:
179
+
180
+ accounts = Account.findwp(params[:search])
181
+ accounts = Account.allwp(params[:search])
182
+ account = Account.firstwp(params[:search])
183
+
184
+ This performs various checks to ensure SQL injection is impossible. I'm sure you know this, but I have to say it: *DO NOT* pass params into the "find", "all", or "first" methods, otherwise you are opening yourself up to SQL injections.
185
+
186
+ # DO NOT DO THIS!
187
+ accounts = Account.all(params[:search])
188
+
189
+ # OR THIS!
190
+ accounts = Account.all(:conditions => params[:conditions])
191
+
192
+ ## Available Conditions
193
+
194
+ Depending on the type, each column comes preloaded with a bunch of nifty conditions:
195
+
196
+ all columns
197
+ => :equals, :does_not_equal
198
+
199
+ :string, :text
200
+ => :begins_with, :contains, :keywords, :ends_with
201
+
202
+ :integer, :float, :decimal,:datetime, :timestamp, :time, :date
203
+ => :greater_than, :greater_than_or_equal_to, :less_than, :less_than_or_equal_to
204
+
205
+ tree data structures (see above "searching trees")
206
+ => :child_of, :sibling_of, :descendent_of, :inclusive_descendent_of
207
+
208
+ Some of these conditions come with aliases, so you have your choice how to call the conditions. For example you can use "greater\_than" or "gt":
209
+
210
+ :equals; => :is
211
+ :does_not_equal => :is_not, :not
212
+ :begins_with => :starts_with
213
+ :contains => :like
214
+ :greater_than => :gt, :after
215
+ :greater_than_or_equal_to => :at_least, :gte
216
+ :less_than => :lt, :before
217
+ :less_than_or_equal_to => :at_most, :lte
218
+
219
+ ### Enhanced searching and blacklisted words
220
+
221
+ You will notice above there is "contains" and "keywords". The difference is that "keywords" is an enhanced search. It acts like a real keyword search. It finds those keywords, in any order, and blacklists meaningless words such as "and", "the", etc. "contains" finds the EXACT string in the column you are searching, spaces and all.
222
+
223
+ ## Creating your search form
224
+
225
+ After all of that, here's why I love this plugin:
226
+
227
+ <% form_for :search, User.new_conditions, :url => users_path do |f| %>
228
+ <%= f.text_field :first_name_contains %>
229
+ <%= f.calendar_date_select :created_at_after %>
230
+ <%= f.select :age_gt, (1..100) %>
231
+ <% end %>
232
+
233
+ ## Credits
234
+
235
+ Author: [Ben Johnson](http://github.com/binarylogic) of [Binary Logic](http://www.binarylogic.com)
236
+
237
+ Credit to [Zack Ham](http://github.com/zackham) and [Robert Malko](http://github.com/malkomalko/) for helping with feature suggestions, cleaning up the readme / wiki, and cleaning up my code.
238
+
239
+
240
+ Copyright (c) 2008 [Ben Johnson](http://github.com/binarylogic) of [Binary Logic](http://www.binarylogic.com), released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'echoe'
3
+
4
+ require File.dirname(__FILE__) << "/lib/searchgasm/version"
5
+
6
+ Echoe.new 'searchgasm' do |p|
7
+ p.version = BinaryLogic::Searchgasm::Version::STRING
8
+ p.author = "Ben Johnson of Binary Logic"
9
+ p.email = 'bjohnson@binarylogic.com'
10
+ p.project = 'searchgasm'
11
+ p.summary = "Orgasmic ActiveRecord searching"
12
+ p.description = "Makes ActiveRecord searching easier, robust, and powerful. Automatic conditions, pagination support, object based searching, and more."
13
+ p.url = "http://github.com/binarylogic/searchgasm"
14
+ p.dependencies = %w(activerecord)
15
+ p.include_rakefile = true
16
+ end
17
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) << "/lib/searchgasm"
@@ -0,0 +1,56 @@
1
+ module BinaryLogic
2
+ module Searchgasm
3
+ module ActiveRecord
4
+ module Associations
5
+ module AssociationCollection
6
+ def self.included(klass)
7
+ klass.class_eval do
8
+ include Protection
9
+ end
10
+ end
11
+
12
+ def find_with_searchgasm(*args)
13
+ options = args.extract_options!
14
+ args << sanitize_options_with_searchgasm(options)
15
+ find_without_searchgasm(*args)
16
+ end
17
+
18
+ def build_conditions(options = {}, &block)
19
+ @reflection.klass.build_conditions(options.merge(:scope => scope(:find)[:conditions]), &block)
20
+ end
21
+
22
+ def build_search(options = {}, &block)
23
+ @reflection.klass.build_search(options.merge(:scope => scope(:find)[:conditions]), &block)
24
+ end
25
+ end
26
+
27
+ module HasManyAssociation
28
+ def count_with_searchgasm(*args)
29
+ column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
30
+ count_without_searchgasm(column_name, sanitize_options_with_searchgasm(options))
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ ActiveRecord::Associations::AssociationCollection.send(:include, BinaryLogic::Searchgasm::ActiveRecord::Associations::AssociationCollection)
39
+
40
+ module ::ActiveRecord
41
+ module Associations
42
+ class AssociationCollection
43
+ alias_method_chain :find, :searchgasm
44
+ end
45
+ end
46
+ end
47
+
48
+ ActiveRecord::Associations::HasManyAssociation.send(:include, BinaryLogic::Searchgasm::ActiveRecord::Associations::HasManyAssociation)
49
+
50
+ module ::ActiveRecord
51
+ module Associations
52
+ class HasManyAssociation
53
+ alias_method_chain :count, :searchgasm
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,70 @@
1
+ module BinaryLogic
2
+ module Searchgasm
3
+ module ActiveRecord
4
+ module Base
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ alias_method :new_conditions, :build_conditions
8
+ alias_method :new_search, :build_search
9
+ include Protection
10
+ end
11
+ end
12
+
13
+ def calculate_with_searchgasm(*args)
14
+ options = args.extract_options!
15
+ options = sanitize_options_with_searchgasm(options)
16
+ args << options
17
+ calculate_without_searchgasm(*args)
18
+ end
19
+
20
+ def find_with_searchgasm(*args)
21
+ options = args.extract_options!
22
+ options = sanitize_options_with_searchgasm(options)
23
+ args << options
24
+ find_without_searchgasm(*args)
25
+ end
26
+
27
+ def scope_with_searchgasm(method, key = nil)
28
+ scope = scope_without_searchgasm(method, key)
29
+ return sanitize_options_with_searchgasm(scope) if key.nil? && method == :find
30
+ scope
31
+ end
32
+
33
+ def build_conditions(options = {})
34
+ BinaryLogic::Searchgasm::Search::Conditions(self, options)
35
+ end
36
+
37
+ def build_search(options = {})
38
+ searcher = searchgasm_searcher(options)
39
+ yield searcher if block_given?
40
+ searcher
41
+ end
42
+
43
+ private
44
+ def sanitize_options_with_searchgasm(options)
45
+ searchgasm_searcher(options).sanitize
46
+ end
47
+
48
+ def searchgasm_searcher(options)
49
+ BinaryLogic::Searchgasm::Search::Base.new(self, options)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ ActiveRecord::Base.send(:extend, BinaryLogic::Searchgasm::ActiveRecord::Base)
57
+
58
+ module ::ActiveRecord
59
+ class Base
60
+ class << self
61
+ alias_method_chain :calculate, :searchgasm
62
+ alias_method_chain :find, :searchgasm
63
+ alias_method_chain :scope, :searchgasm
64
+
65
+ def valid_find_options
66
+ VALID_FIND_OPTIONS
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,37 @@
1
+ module BinaryLogic
2
+ module Searchgasm
3
+ module ActiveRecord
4
+ module Protection
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ alias_method :new_search, :build_search
8
+ alias_method :findwp, :find_with_protection
9
+ alias_method :allwp, :all_with_protection
10
+ alias_method :firstwp, :first_with_protection
11
+ end
12
+ end
13
+
14
+ def find_with_protection(*args)
15
+ options = args.extract_options!
16
+ options.merge!(:protect => true)
17
+ args << options
18
+ find(*args)
19
+ end
20
+
21
+ def all_with_protection(*args)
22
+ options = args.extract_options!
23
+ options.merge!(:protect => true)
24
+ args << options
25
+ all(*args)
26
+ end
27
+
28
+ def first_with_protection(*args)
29
+ options = args.extract_options!
30
+ options.merge!(:protect => true)
31
+ args << options
32
+ first(*args)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,100 @@
1
+ require "base64"
2
+
3
+ module BinaryLogic
4
+ module SearchGasm
5
+ module Helpers
6
+ def order_by_link(text, searcher, options = {})
7
+ options[:order_by] ||= text.underscore.gsub(/ /, "_")
8
+ options[:order_as] ||= "ASC"
9
+ options[:form_prefix] ||= determine_form_prefix(searcher)
10
+
11
+ if searcher.order_by == options[:order_by]
12
+ options[:order_as] = searcher.order_as == "ASC" ? "DESC" : "ASC"
13
+
14
+ text = content_tag("span", text + (searcher.order_as == "ASC" ? "&nbsp;&#9650;" : "&nbsp;&#9660;"), :class => searcher.order_as == "ASC" ? "ordering asc" : "ordering desc")
15
+ end
16
+
17
+ options[:order_by] = Base64.encode64(Marshal.dump(options[:order_by])) if !options[:order_by].nil? && !options[:order_by].is_a?(String) && !options[:order_by].is_a?(Symbol)
18
+
19
+ link_to_function(text, "submit_form({form_prefix: '#{options[:form_prefix]}', dont_reset: true, fields: {order_by: '#{escape_javascript(options[:order_by])}', order_as: '#{options[:order_as]}'}});")
20
+ end
21
+
22
+ # tag methods
23
+ #------------------------------------------------------------------------------
24
+ def page_select_tag(name, items_count, searcher, options = {})
25
+ options = options.dup
26
+ form_prefix = options.delete(:form_prefix) || determine_form_prefix(searcher)
27
+ options[:id] ||= "select_tag_#{unique_id}"
28
+ options[:onchange] ||= "submit_form({form_prefix: '#{form_prefix}', dont_reset: true, fields: {page: this.value}});"
29
+ items_count = items_count.to_i
30
+ per_page = searcher.per_page.to_i
31
+ page = searcher.page.to_i
32
+ page = 1 if page < 1
33
+ page_options = page_options_for_select(items_count, per_page)
34
+
35
+ html = ""
36
+ if page_options.size > 0
37
+ html << (button_to_function("Prev", "sel = $('#{options[:id]}'); sel.value = '#{page-1}'; sel.onchange();", :class => "prev_page") + "&nbsp;") if page > 1
38
+ html << select_tag(name, options_for_select(page_options, page), options)
39
+ html << ("&nbsp;" + button_to_function("Next", "sel = $('#{options[:id]}'); sel.value = '#{page+1}'; sel.onchange();", :class => "next_page")) if page < page_options.size
40
+ end
41
+
42
+ html
43
+ end
44
+
45
+ def per_page_select_tag(name, items_count, searcher, options = {})
46
+ options = options.dup
47
+ form_prefix = options.delete(:form_prefix) || determine_form_prefix(searcher)
48
+ options[:onchange] ||= "submit_form({form_prefix: '#{form_prefix}', dont_reset: true, fields: {per_page: this.value}});"
49
+ items_count = items_count.to_i
50
+ per_page = searcher.per_page.to_i
51
+ per_page = 0 if items_count <= per_page
52
+
53
+ # set up per page options
54
+ per_page_options = per_page_options_for_select(items_count)
55
+
56
+ return select_tag(name, options_for_select(per_page_options, per_page), options) if per_page_options.size > 0
57
+
58
+ ""
59
+ end
60
+
61
+ # utility methods
62
+ #------------------------------------------------------------------------------
63
+ def page_options_for_select(items_count, per_page)
64
+ page_count = per_page > 0 ? (items_count.to_f / per_page.to_f).ceil : 1
65
+ page_options = []
66
+ if page_count > 1
67
+ page_count.times do |page|
68
+ page_number = page + 1
69
+ page_options << ["Page #{page_number} of #{page_count}", page_number]
70
+ end
71
+ end
72
+ page_options
73
+ end
74
+
75
+ def per_page_options_for_select(items_count)
76
+ per_page_options = []
77
+ per_page_values = [10, 20, 30, 40, 50, 75, 100, 125, 150, 175, 200, 300, 400, 500, 1000, 1500, 2000]
78
+ per_page_values.each do |per_page_num|
79
+ if items_count > per_page_num
80
+ per_page_options << ["#{per_page_num} per page", per_page_num] if per_page_num > 0
81
+ else
82
+ break
83
+ end
84
+ end
85
+
86
+ if per_page_options.size > 0
87
+ per_page_options << ["Show all #{items_count}", 0]
88
+ end
89
+
90
+ per_page_options
91
+ end
92
+
93
+ def determine_form_prefix(searcher)
94
+ "#{searcher.class.name.underscore.gsub(/_searcher/, "").pluralize}_search_"
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ ActionController::Base.helper BinaryLogic::SearchGasm::Helpers