Skip to content
Browse files

Released verversion 0.9.7

  • Loading branch information...
1 parent c4a3956 commit 6449698e3b3b57ca007cd1b5bc256ba331e94af7 @binarylogic committed Sep 6, 2008
Showing with 1,938 additions and 3,086 deletions.
  1. +2 −0 CHANGELOG
  2. +32 −1,704 Manifest
  3. +80 −65 README.mdown → README.rdoc
  4. +4 −0 examples/README.rdoc
  5. +0 −23 examples/index.html.erb
  6. +0 −23 examples/index.html.haml
  7. +0 −6 examples/user_controller.rb
  8. +58 −27 lib/searchgasm.rb
  9. +2 −2 lib/searchgasm/active_record/associations.rb
  10. +4 −4 lib/searchgasm/active_record/base.rb
  11. +30 −6 lib/searchgasm/{search/condition.rb → condition/base.rb}
  12. +20 −0 lib/searchgasm/condition/begins_with.rb
  13. +11 −0 lib/searchgasm/condition/child_of.rb
  14. +20 −0 lib/searchgasm/condition/contains.rb
  15. +24 −0 lib/searchgasm/condition/descendant_of.rb
  16. +28 −0 lib/searchgasm/condition/does_not_equal.rb
  17. +20 −0 lib/searchgasm/condition/ends_with.rb
  18. +20 −0 lib/searchgasm/condition/equals.rb
  19. +25 −0 lib/searchgasm/condition/greater_than.rb
  20. +20 −0 lib/searchgasm/condition/greater_than_or_equal_to.rb
  21. +13 −0 lib/searchgasm/condition/inclusive_descendant_of.rb
  22. +33 −0 lib/searchgasm/condition/keywords.rb
  23. +25 −0 lib/searchgasm/condition/less_than.rb
  24. +20 −0 lib/searchgasm/condition/less_than_or_equal_to.rb
  25. +16 −0 lib/searchgasm/condition/sibling_of.rb
  26. +16 −0 lib/searchgasm/condition/tree.rb
  27. +221 −0 lib/searchgasm/conditions/base.rb
  28. +30 −0 lib/searchgasm/conditions/protection.rb
  29. +137 −0 lib/searchgasm/config.rb
  30. +82 −57 lib/searchgasm/helpers/form_helper.rb
  31. +0 −37 lib/searchgasm/helpers/order_helper.rb
  32. +0 −69 lib/searchgasm/helpers/pagination_helper.rb
  33. +178 −0 lib/searchgasm/helpers/search_helper.rb
  34. +113 −32 lib/searchgasm/helpers/utilities_helper.rb
  35. +26 −237 lib/searchgasm/search/base.rb
  36. +0 −24 lib/searchgasm/search/condition_types/begins_with_condition.rb
  37. +0 −15 lib/searchgasm/search/condition_types/child_of_condition.rb
  38. +0 −24 lib/searchgasm/search/condition_types/contains_condition.rb
  39. +0 −28 lib/searchgasm/search/condition_types/descendant_of_condition.rb
  40. +0 −32 lib/searchgasm/search/condition_types/does_not_equal_condition.rb
  41. +0 −24 lib/searchgasm/search/condition_types/ends_with_condition.rb
  42. +0 −24 lib/searchgasm/search/condition_types/equals_condition.rb
  43. +0 −29 lib/searchgasm/search/condition_types/greater_than_condition.rb
  44. +0 −24 lib/searchgasm/search/condition_types/greater_than_or_equal_to_condition.rb
  45. +0 −17 lib/searchgasm/search/condition_types/inclusive_descendant_of_condition.rb
  46. +0 −37 lib/searchgasm/search/condition_types/keywords_condition.rb
  47. +0 −29 lib/searchgasm/search/condition_types/less_than_condition.rb
  48. +0 −24 lib/searchgasm/search/condition_types/less_than_or_equal_to_condition.rb
  49. +0 −20 lib/searchgasm/search/condition_types/sibling_of_condition.rb
  50. +0 −18 lib/searchgasm/search/condition_types/tree_condition.rb
  51. +28 −162 lib/searchgasm/search/conditions.rb
  52. +149 −0 lib/searchgasm/search/ordering.rb
  53. +69 −0 lib/searchgasm/search/pagination.rb
  54. +61 −0 lib/searchgasm/search/protection.rb
  55. +0 −32 lib/searchgasm/search/utilities.rb
  56. +30 −0 lib/searchgasm/utilities.rb
  57. +3 −2 lib/searchgasm/version.rb
  58. +2 −2 test/test_active_record_base.rb
  59. +30 −30 test/{test_searchgasm_condition_types.rb → test_condition.rb}
  60. +37 −37 test/{test_searchgasm_conditions.rb → test_conditions_base.rb}
  61. +37 −160 test/{test_searchgasm_base.rb → test_search_base.rb}
  62. +91 −0 test/test_search_ordering.rb
  63. +56 −0 test/test_search_pagination.rb
  64. +35 −0 test/test_search_protection.rb
View
2 CHANGELOG
@@ -1,3 +1,5 @@
+v0.9.7. Complete class restructure, much more organized, added in documentation, added in helpers for using searchgasm in a rails app, updated readme with link to documentation as well as a live example, some bug fixes, more tests
+
v0.9.6. Fix bug when instantiating with nil options
v0.9.5. Enhanced searching with conditions only. Updated read me to include example on adding your own conditions.
View
1,736 Manifest
32 additions, 1,704 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
145 README.mdown → README.rdoc
@@ -1,27 +1,53 @@
-# Searchgasm
+= Searchgasm
Searchgasm is orgasmic. Maybe not orgasmic, but you will get aroused. So go grab a towel and let's dive in.
-**Searchgasm's inspiration comes right from ActiveRecord. ActiveRecord lets you create objects that represent a record in the database, so why can't you create objects that represent searching the database? Now you can!**
+<b>Searchgasm's inspiration comes right from ActiveRecord. ActiveRecord lets you create objects that represent a record in the database, so why can't you create objects that represent searching the database? Now you can!</b>
-The best part is that rails already has all kinds of nifty methods to make dealing with ActiveRecord objects quick and easy, especially with forms. So let's take advantage of them! That's the idea behind this plugin. Searchgasm is searching, ordering, and pagination all rolled into one simple plugin. Take all of that pagination and searching cruft out of your models and let searchgasm handle it.
+== Under the hood
-## How it works
+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 "sanitizes" down into the options passed into ActiveRecord::Base.find(). It 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. It doesn't dig into the ActiveRecord internals, it only uses what is publicly available. It jumps in and helps out <em>only</em> when needed, otherwise it sits back and lets ActiveRecord do all of the work. Between that and the extensive tests, this is a solid and fast plugin.
-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 "sanitizes" 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. It doesn't dig into the ActiveRecord internals, it only uses what is publicly available. It jumps in and helps out *only* when needed, otherwise it sits back and lets ActiveRecord do all of the work. Between that and the extensive tests, this is a solid and fast plugin.
+== Quicklinks
-## The beauty of searchgasm
+* <b>Documentation:</b> http://searchgasm.rubyforge.org
+* <b>Live rails example:</b> http://searchgasm_example.binarylogic.com
+
+== Install and use
+
+ sudo gem install searchgasm
+
+For rails > 2.1
+
+ # environment.rb
+ config.gem "searchgasm"
+
+For rails < 2.1
+
+ # environment.rb
+ require "searchgasm"
+
+Or as a plugin
+
+ script/plugin install git://github.com/binarylogic/searchgasm.git
+
+Now try out some of the examples below:
+
+<b>For all examples, let's assume the following relationships: User => Orders => Line Items</b>
+
+== The beauty of searchgasm
+
+Using Searchgasm in rails is the best part, because rails has all kinds of nifty methods to make dealing with ActiveRecord objects quick and easy, especially with forms. So let's take advantage of them! That's the idea behind this plugin. Searchgasm is searching, ordering, and pagination all rolled into one simple plugin. Take all of that pagination and searching cruft out of your models and let Searchgasm handle it. Check it out:
# app/controllers/users_controller.rb
def index
@search = User.new_search(params[:search])
@users, @users_count = @search.all, @search.count
end
-Now your view. Notice you can use your search object *just like* an ActiveRecord object.
+Now your view. Notice you can use your search object <b>just like</b> an ActiveRecord object.
# app/views/users/index.html.haml
- # This example is in haml because github is screwing up my HTML
- form_for @search do |f|
- f.fields_for @search.conditions do |users|
= users.text_field :first_name_contains
@@ -46,31 +72,11 @@ Now your view. Notice you can use your search object *just like* an ActiveRecord
Page:
= page
-Nice and simple. 2 lines in your controller, no "cruft" in your models, pagination as simple as calling the "page" and "per\_page" helpers, and ordering as simple as calling "order\_by". All of these methods come with a list of options (see Searchgasm::Search::Helpers). That's it!
-
-## Install and use
+Nice and simple. 2 lines in your controller, no "cruft" in your models, pagination as simple as calling the "page" and "per_page" helpers, ordering as simple as calling "order_by", and all searching conditions in 1 spot: your form. For documentation on helpers see Searchgasm::Helpers::FormHelper and Searchgasm::Helpers::SearchHelper.
- sudo gem install searchgasm
-
-For rails > 2.1
-
- # environment.rb
- config.gem "searchgasm"
-
-For rails < 2.1
-
- # environment.rb
- require "searchgasm"
+<b>See this example live: http://searchgasm_example.binarylogic.com</b>
-Or as a plugin
-
- script/plugin install git://github.com/binarylogic/searchgasm.git
-
-Now go into your console and try out any of these example with your own models.
-
-**For all examples, let's assume the following relationships: User => Orders => Line Items**
-
-## Simple Searching Example
+== Simple Searching Example
User.all(
:conditions => {
@@ -81,9 +87,9 @@ Now go into your console and try out any of these example with your own models.
:page => 3 # offset 40, which starts us on page 3
)
-Instead of using the "all" method you could use any search method: first, find(:all), find(:first), count, sum, average, etc
+Instead of using the "all" method you could use any search method: first, find(:all), find(:first), count, sum, average, etc, just like ActiveRecord
-## Detailed Example w/ object based searching (great for form\_for or fields\_for)
+== Exhaustive Example w/ Object Based Searching (great for form_for or fields_for)
# Instantiate
@search = User.new_search(
@@ -98,12 +104,16 @@ Instead of using the "all" method you could use any search method: first, find(:
:order_as => "DESC"
)
- # Set conditions on relationships
+ # Set local conditions
@search.conditions.email_ends_with = "binarylogic.com"
- @search.conditions.oders.line_items.created_after = Time.now
+
+ # Set conditions on relationships
+ @search.conditions.oders.line_items.created_after = Time.now # can traverse through all relationships
# Set options
- @search.per_page = 50 # overrides the 20 set above
+ @search.per_page = 50 # overrides the 20 set above
+ @search.order_by = [:first_name, {:user_group => :name}] # order by first name and then by the user group's name it belongs to
+ @search.order_as = "ASC"
# Set ANY of the ActiveRecord options
@search.group = "last_name"
@@ -112,12 +122,11 @@ Instead of using the "all" method you could use any search method: first, find(:
# Return results just like ActiveRecord
@search.all
- @search.search # alias for all
@search.first
Take the @search object and pass it right into form\_for or fields\_for (see above).
-## Calculations
+== Calculations
Using the object from above:
@@ -126,15 +135,16 @@ Using the object from above:
@search.maximum('id')
@search.minimum('id')
@search.sum('id')
- @search.calculate(:sum, 'id') # any of the above calculations
+ @search.calculate(:sum, 'id')
+ # ...any of the above calculations, see ActiveRecord documentation on calculations
Or do it from your model:
User.count(:conditions => {:first_name_contains => "Ben"})
User.sum('id', :conditions => {:first_name_contains => "Ben"})
# ... all other calcualtions, etc.
-## Different ways to search, take your pick
+== Different ways to search, take your pick
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:
@@ -151,42 +161,41 @@ Any of the options used in the above example can be used in these, but for the s
search.per_page = 20
search.all
-If you want to be hardcore:
+If you want to use Searchgasm directly:
- search = Searchgasm::Search.new(User, :conditions => {:age_gt => 18})
+ search = Searchgasm::Search::Base.new(User, :conditions => {:age_gt => 18})
search.conditions.first_name_contains = "Ben"
search.per_page = 20
search.all
-## Search with conditions only
+== Search with conditions only
conditions = User.new_conditions(:age_gt => 18)
conditions.first_name_contains = "Ben"
- conditions.search
conditions.all
# ... all operations above are available
Pass a conditions object right into ActiveRecord:
- User.all(:conditions => conditions) # same as conditions.search
+ User.all(:conditions => conditions)
-Again, if you want to be hardcore:
+Again, if you want to use Searchgasm directly:
- conditions = Searchgasm::Search::Conditions.new(User, :age_gt => 18)
+ conditions = Searchgasm::Conditions::Base.new(User, :age_gt => 18)
conditions.first_name_contains = "Ben"
- conditions.search
+ conditions.all
Now pass the conditions object right into form\_for or fields\_for (see above for example).
-## Scoped searching
+== Scoped searching
@current_user.orders.find(:all, :conditions => {:total_lte => 500})
@current_user.orders.count(:conditions => {:total_lte => 500})
@current_user.orders.sum('total', :conditions => {:total_lte => 500})
search = @current_user.orders.build_search('total', :conditions => {:total_lte => 500})
-## Searching trees
+== Searching trees
For tree data structures you get a few nifty methods. Let's assume Users is a tree data structure.
@@ -207,25 +216,25 @@ For tree data structures you get a few nifty methods. Let's assume Users is a tr
User.all(:conditions => {:inclusive_descendant_of => User.roots.first.id})
-## Available anywhere (relationships & named scopes)
+== Available anywhere (relationships & scopes)
-Not only can you use searchgasm when searching, but you can use it when setting up relationships or named scopes:
+Not only can you use searchgasm when searching, but you can use it when setting up relationships or scopes. Anywhere you specify conditions in ActiveRecord.
class User < ActiveRecord::Base
has_many :expensive_pending_orders, :conditions => {:total_greater_than => 1_000_000, :state => :pending}, :per_page => 20
named_scope :sexy, :conditions => {:first_name => "Ben", email_ends_with => "binarylogic.com"}, :per_page => 20
end
-## Always use protection...against SQL injections
+== Always use protection...against SQL injections
-If there is one thing we all know, it's to always use protection. That's why searchgasm protects you by default. The new\_search and new\_conditions methods are protected by default. This means that various checks are done to ensure it is not possible to perform any type of SQL injection. But this also limits how you can search, meaning you can't write raw SQL. If you want to be daring and search without protection prepare to accept the consequences. All that you have to do is add ! to the end of the methods: new\_search! and new\_conditions!.
+If there is one thing we all know, it's to always use protection against SQL injections. That's why searchgasm protects you by default. The new\_search and new\_conditions methods are protected by default. This means that various checks are done to ensure it is not possible to perform any type of SQL injection. But this also limits how you can search, meaning you can't write raw SQL. If you want to be daring and search without protection, all that you have to do is add ! to the end of the methods: new\_search! and new\_conditions!.
-### Protected from SQL injections
+=== Protected from SQL injections
search = Account.new_search(params[:search])
conditions = Account.new_conditions(params[:conditions])
-### *NOT* protected from SQL injections
+=== *NOT* protected from SQL injections
accounts = Account.find(params[:search])
accounts = Account.all(params[:search])
@@ -235,7 +244,7 @@ If there is one thing we all know, it's to always use protection. That's why sea
Lesson learned: use new\_search and new\_conditions when passing in params as *ANY* of the options.
-## Available Conditions
+== Available Conditions
Depending on the type, each column comes preloaded with a bunch of nifty conditions:
@@ -264,19 +273,21 @@ Some of these conditions come with aliases, so you have your choice how to call
:less_than => :lt, :before
:less_than_or_equal_to => :at_most, :lte
-### Enhanced searching and blacklisted words
+For more information on each condition see Searchgasm::Condition. Each condition has it's own class and the source is pretty simple and self explanatory.
+
+=== Enhanced searching and blacklisted words
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.
-### Roll your own conditions
+=== Roll your own conditions
I didn't include this function because its MySQL specific, and it's probably rarely used, but MySQL supports a "SOUNDS LIKE" function.
I want to use it, so let's add it:
# config/initializers/searchgasm.rb
# Actual function for MySQL databases only
- class SoundsLikeCondition < Searchgasm::Search::Condition
+ class SoundsLike < Searchgasm::Condition::Base
class << self
# I pass you the column, you tell me what you want the method to be called.
# If you don't want to add this condition for that column, return nil
@@ -299,21 +310,25 @@ I want to use it, so let's add it:
end
end
- Searchgasm::Seearch::Conditions.register_condition(SoundsLikeCondition)
+ Searchgasm::Conditions::Base.register_condition(SoundsLike)
Now test it out:
search = User.new_search
- search.first_name_sounds_like = "Ben"
+ search.conditions.first_name_sounds_like = "Ben"
search.all # will return any user that has a first name that sounds like "Ben"
-Pretty nifty, huh? You can create any condition ultimately creating any SQL you want. The sky is the limit.
+Pretty nifty, huh? You can create any condition ultimately creating any SQL you want. The sky is the limit. For more information see Searchgasm::Condition::Base
+
+== Reporting problems / bugs
+
+http://binarylogic.lighthouseapp.com/projects/16601-searchgasm
-## Credits
+== Credits
Author: [Ben Johnson](http://github.com/binarylogic) of [Binary Logic](http://www.binarylogic.com)
-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.
+Credit to [Zack Ham](http://github.com/zackham) and [Robert Malko](http://github.com/malkomalko/) for helping with feature suggestions.
Copyright (c) 2008 [Ben Johnson](http://github.com/binarylogic) of [Binary Logic](http://www.binarylogic.com), released under the MIT license
View
4 examples/README.rdoc
@@ -0,0 +1,4 @@
+=== Live example
+
+* Please see the live example: http://searchgasm_example.binarylogic.com
+* Source / Github project for the example: http://github.com/binarylogic/searchgasm_example.binarylogic.com
View
23 examples/index.html.erb
@@ -1,23 +0,0 @@
-<% form_for @search do |f| %>
- <% f.fields_for @search.conditions do |users| %>
- <%= users.text_field :first_name_contains %>
- <%= users.calendar_date_select :created_after %> <!-- # nice rails plugin for replacing date_select -->
- <% users.fields_for users.object.orders do |orders| %>
- <%= orders.select :total_gt, (1..100) %>
- <%= f.submit "Search" %>
-
-<table>
- <tr>
- <th><%= order_by :first_name %></th>
- <th><%= order_by :last_name %></th>
- <th><%= order_by :email %></th>
- <% @users.each do |user| %>
- <tr>
- <td><%= user.first_name %></td>
- %td= user.last_name
- %td= user.email
-
-Per page:
-= per_page
-Page:
-= page
View
23 examples/index.html.haml
@@ -1,23 +0,0 @@
-- form_for @search do |f|
- - f.fields_for @search.conditions do |users|
- = users.text_field :first_name_contains
- = users.calendar_date_select :created_after # nice rails plugin for replacing date_select and datetime_select
- - users.fields_for users.object.orders do |orders|
- = orders.select :total_gt, (1..100)
- = f.submit "Search"
-
-%table
- %tr
- %th= order_by :first_name
- %th= order_by :last_name
- %th= order_by :email
- - @users.each do |user|
- %tr
- %td= user.first_name
- %td= user.last_name
- %td= user.email
-
-Per page:
-= per_page
-Page:
-= page
View
6 examples/user_controller.rb
@@ -1,6 +0,0 @@
-class UserController < ApplicationController
- def index
- @search = User.new_search(params[:search])
- @users, @users_count = @search.all, @search.count
- end
-end
View
85 lib/searchgasm.rb
@@ -1,36 +1,67 @@
require "active_record"
+require "active_support"
+# Utilties
+require "searchgasm/version"
+require "searchgasm/config"
+require "searchgasm/utilities"
+
+# ActiveRecord
require "searchgasm/active_record/base"
require "searchgasm/active_record/associations"
-# Helpers
-require "searchgasm/helpers/utilities_helper"
-require "searchgasm/helpers/form_helper"
-require "searchgasm/helpers/order_helper"
-require "searchgasm/helpers/pagination_helper"
-
-# Core
-require "searchgasm/version"
-require "searchgasm/search/utilities"
-require "searchgasm/search/condition"
+# Search
+require "searchgasm/search/ordering"
+require "searchgasm/search/pagination"
require "searchgasm/search/conditions"
require "searchgasm/search/base"
+require "searchgasm/search/protection"
+
+# Conditions
+require "searchgasm/conditions/protection"
+require "searchgasm/conditions/base"
+
+# Condition
+require "searchgasm/condition/base"
+require "searchgasm/condition/begins_with"
+require "searchgasm/condition/contains"
+require "searchgasm/condition/does_not_equal"
+require "searchgasm/condition/ends_with"
+require "searchgasm/condition/equals"
+require "searchgasm/condition/greater_than"
+require "searchgasm/condition/greater_than_or_equal_to"
+require "searchgasm/condition/keywords"
+require "searchgasm/condition/less_than"
+require "searchgasm/condition/less_than_or_equal_to"
+require "searchgasm/condition/tree"
+require "searchgasm/condition/child_of"
+require "searchgasm/condition/descendant_of"
+require "searchgasm/condition/inclusive_descendant_of"
+require "searchgasm/condition/sibling_of"
-# Regular conidtion types
-require "searchgasm/search/condition_types/begins_with_condition"
-require "searchgasm/search/condition_types/contains_condition"
-require "searchgasm/search/condition_types/does_not_equal_condition"
-require "searchgasm/search/condition_types/ends_with_condition"
-require "searchgasm/search/condition_types/equals_condition"
-require "searchgasm/search/condition_types/greater_than_condition"
-require "searchgasm/search/condition_types/greater_than_or_equal_to_condition"
-require "searchgasm/search/condition_types/keywords_condition"
-require "searchgasm/search/condition_types/less_than_condition"
-require "searchgasm/search/condition_types/less_than_or_equal_to_condition"
+# Helpers
+require "searchgasm/helpers/utilities_helper"
+require "searchgasm/helpers/form_helper"
+require "searchgasm/helpers/search_helper"
-# Tree condition types
-require "searchgasm/search/condition_types/tree_condition"
-require "searchgasm/search/condition_types/child_of_condition"
-require "searchgasm/search/condition_types/descendant_of_condition"
-require "searchgasm/search/condition_types/inclusive_descendant_of_condition"
-require "searchgasm/search/condition_types/sibling_of_condition"
+# Lets do it!
+module Searchgasm
+ module Search
+ class Base
+ include Conditions
+ include Ordering
+ include Pagination
+ include Protection
+ end
+ end
+
+ module Conditions
+ class Base
+ include Protection
+ end
+
+ [:begins_with, :child_of, :contains, :descendant_of, :does_not_equal, :ends_with, :equals, :greater_than, :greater_than_or_equal_to, :inclusive_descendant_of, :keywords, :less_than, :less_than_or_equal_to, :sibling_of].each do |condition|
+ Base.register_condition("Searchgasm::Condition::#{condition.to_s.camelize}".constantize)
+ end
+ end
+end
View
4 lib/searchgasm/active_record/associations.rb
@@ -45,7 +45,7 @@ def count_with_searchgasm(*args)
ActiveRecord::Associations::AssociationCollection.send(:include, Searchgasm::ActiveRecord::Associations::AssociationCollection)
-module ::ActiveRecord
+module ActiveRecord
module Associations
class AssociationCollection
alias_method_chain :find, :searchgasm
@@ -55,7 +55,7 @@ class AssociationCollection
ActiveRecord::Associations::HasManyAssociation.send(:include, Searchgasm::ActiveRecord::Associations::HasManyAssociation)
-module ::ActiveRecord
+module ActiveRecord
module Associations
class HasManyAssociation
alias_method_chain :count, :searchgasm
View
8 lib/searchgasm/active_record/base.rb
@@ -1,5 +1,5 @@
module Searchgasm
- module ActiveRecord
+ module ActiveRecord #:nodoc: all
module Base
def calculate_with_searchgasm(*args)
options = args.extract_options!
@@ -24,7 +24,7 @@ def scope_with_searchgasm(method, key = nil)
def build_conditions(values = {}, &block)
conditions = searchgasm_conditions
conditions.protect = true
- conditions.value = values
+ conditions.conditions = values
yield conditions if block_given?
conditions
end
@@ -72,7 +72,7 @@ def sanitize_options_with_searchgasm(options = {})
end
def searchgasm_conditions(options = {})
- Searchgasm::Search::Conditions.new(self, options)
+ Searchgasm::Conditions::Base.new(self, options)
end
def searchgasm_searcher(options = {})
@@ -84,7 +84,7 @@ def searchgasm_searcher(options = {})
ActiveRecord::Base.send(:extend, Searchgasm::ActiveRecord::Base)
-module ::ActiveRecord
+module ActiveRecord #:nodoc: all
class Base
class << self
alias_method_chain :calculate, :searchgasm
View
36 lib/searchgasm/search/condition.rb → lib/searchgasm/condition/base.rb
@@ -1,36 +1,49 @@
module Searchgasm
- module Search
- class Condition
+ module Condition # :nodoc:
+ # = Conditions condition
+ #
+ # The base class for creating a condition. Your custom conditions should extend this class.
+ # See Searchgasm::Conditions::Base.register_condition on how to write your own condition.
+ class Base
include Utilities
-
+
attr_accessor :column, :klass
attr_reader :value
class << self
+ # Name of the condition inferred from the class name
def condition_name
- name.split("::").last.scan(/(.*)Condition/)[0][0].underscore
+ name.split("::").last.gsub(/Condition$/, "").underscore
end
+ # I pass you a column you tell me what to call the condition. If you don't want to use this condition for the column
+ # just return nil
def name_for_column(column)
"#{column.name}_#{condition_name}"
end
+ # Alias methods for the column condition.
def aliases_for_column(column)
[]
end
+ # Sane as name_for_column but for the class as a whole. For example the tree methods apply to the class as a whole and not
+ # specific columns. Any condition that applies to columns should probably return nil here.
def name_for_klass(klass)
nil
end
+ # Alias methods for the klass condition
def aliases_for_klass(klass)
[]
end
+ # A utility method for using in name_for_column. For example the keywords condition only applied to string columns, the great than condition doesnt.
def string_column?(column)
[:string, :text].include?(column.type)
end
+ # A utility method for using in name_for_column. For example you wouldn't want a string column to use the greater thann condition, but you would for an integer column.
def comparable_column?(column)
[:integer, :float, :decimal, :datetime, :timestamp, :time, :date].include?(column.type)
end
@@ -41,6 +54,7 @@ def initialize(klass, column = nil)
self.column = column.is_a?(String) ? klass.columns_hash[column] : column
end
+ # Allows nils to be meaninful values
def explicitly_set_value=(value)
@explicitly_set_value = value
end
@@ -49,36 +63,44 @@ def explicitly_set_value=(value)
def explicitly_set_value?
@explicitly_set_value == true
end
-
+
+ # In most cases a blank value should be ignored, except for conditions like equals. A blank value is meaningful there, but a blank value for they keyswords condition is not.
def ignore_blanks?
true
end
+ # A convenience method for the name of the method for that specific column or klass
def name
column ? self.class.name_for_column(column) : self.class.name_for_klass(klass)
end
+ # A convenience method for the name of this condition
def condition_name
self.class.condition_name
end
+ # Quotes a column name properly for sql.
def quote_column_name(column_name)
klass.connection.quote_column_name(column_name)
end
+ # A convenience method for using when writing your sql in to_conditions. This is the proper way to use a column name in a query for most databases
def quoted_column_name
quote_column_name(column.name)
end
+ # Quotes a table name properly for sql
def quote_table_name(table_name)
klass.connection.quote_table_name(table_name)
end
+ # A convenience method for using when writing your sql in to_conditions. This is the proper way to use a table name in a query for most databases
def quoted_table_name
quote_table_name(klass.table_name)
end
- def sanitize(alt_value = nil)
+ # You should refrain from overwriting this method, it performs various tasks before callign your to_conditions method, allowing you to keep to_conditions simple.
+ def sanitize(alt_value = nil) # :nodoc:
return unless explicitly_set_value?
v = alt_value || value
if v.is_a?(Array) && !["equals", "does_not_equal"].include?(condition_name)
@@ -89,10 +111,12 @@ def sanitize(alt_value = nil)
end
end
+ # The value for the condition
def value
@value.is_a?(String) ? column.type_cast(@value) : @value
end
+ # Sets the value for the condition, will ignore place if ignore_blanks?
def value=(v)
return if ignore_blanks? && v.blank?
self.explicitly_set_value = true
View
20 lib/searchgasm/condition/begins_with.rb
@@ -0,0 +1,20 @@
+module Searchgasm
+ module Condition
+ class BeginsWith < Base
+ class << self
+ def name_for_column(column)
+ return unless string_column?(column)
+ super
+ end
+
+ def aliases_for_column(column)
+ ["#{column.name}_bw", "#{column.name}_starts_with", "#{column.name}_start"]
+ end
+ end
+
+ def to_conditions(value)
+ ["#{quoted_table_name}.#{quoted_column_name} LIKE ?", "#{value}%"]
+ end
+ end
+ end
+end
View
11 lib/searchgasm/condition/child_of.rb
@@ -0,0 +1,11 @@
+module Searchgasm
+ module Condition
+ class ChildOf < Tree
+ def to_conditions(value)
+ parent_association = klass.reflect_on_association(:parent)
+ foreign_key_name = (parent_association && parent_association.options[:foreign_key]) || "parent_id"
+ ["#{quoted_table_name}.#{quote_column_name(foreign_key_name)} = ?", (value.is_a?(klass) ? value.send(klass.primary_key) : value)]
+ end
+ end
+ end
+end
View
20 lib/searchgasm/condition/contains.rb
@@ -0,0 +1,20 @@
+module Searchgasm
+ module Condition
+ class Contains < Base
+ class << self
+ def name_for_column(column)
+ return unless string_column?(column)
+ super
+ end
+
+ def aliases_for_column(column)
+ ["#{column.name}_like", "#{column.name}_has"]
+ end
+ end
+
+ def to_conditions(value)
+ ["#{quoted_table_name}.#{quoted_column_name} LIKE ?", "%#{value}%"]
+ end
+ end
+ end
+end
View
24 lib/searchgasm/condition/descendant_of.rb
@@ -0,0 +1,24 @@
+module Searchgasm
+ module Condition
+ class DescendantOf < Tree
+ def to_conditions(value)
+ # Wish I knew how to do this in SQL
+ root = (value.is_a?(klass) ? value : klass.find(value)) rescue return
+ strs = []
+ subs = []
+ all_children_ids(root).each do |child_id|
+ strs << "#{quoted_table_name}.#{quote_column_name(klass.primary_key)} = ?"
+ subs << child_id
+ end
+ [strs.join(" OR "), *subs]
+ end
+
+ private
+ def all_children_ids(record)
+ ids = record.children.collect { |child| child.send(klass.primary_key) }
+ record.children.each { |child| ids += all_children_ids(child) }
+ ids
+ end
+ end
+ end
+end
View
28 lib/searchgasm/condition/does_not_equal.rb
@@ -0,0 +1,28 @@
+module Searchgasm
+ module Condition
+ class DoesNotEqual < Base
+ class << self
+ def aliases_for_column(column)
+ ["#{column.name}_is_not", "#{column.name}_not"]
+ end
+ end
+
+ def ignore_blanks?
+ false
+ end
+
+ def to_conditions(value)
+ # Delegate to equals and then change
+ condition = Equals.new(klass, column)
+ condition.value = value
+
+ sql = condition.sanitize
+ sql.gsub!(/ IS /, " IS NOT ")
+ sql.gsub!(/ BETWEEN /, " NOT BETWEEN ")
+ sql.gsub!(/ IN /, " NOT IN ")
+ sql.gsub!(/=/, "!=")
+ sql
+ end
+ end
+ end
+end
View
20 lib/searchgasm/condition/ends_with.rb
@@ -0,0 +1,20 @@
+module Searchgasm
+ module Condition
+ class EndsWith < Base
+ class << self
+ def name_for_column(column)
+ return unless string_column?(column)
+ super
+ end
+
+ def aliases_for_column(column)
+ ["#{column.name}_ew", "#{column.name}_ends", "#{column.name}_end"]
+ end
+ end
+
+ def to_conditions(value)
+ ["#{quoted_table_name}.#{quoted_column_name} LIKE ?", "%#{value}"]
+ end
+ end
+ end
+end
View
20 lib/searchgasm/condition/equals.rb
@@ -0,0 +1,20 @@
+module Searchgasm
+ module Condition
+ class Equals < Base
+ class << self
+ def aliases_for_column(column)
+ ["#{column.name}", "#{column.name}_is"]
+ end
+ end
+
+ def ignore_blanks?
+ false
+ end
+
+ def to_conditions(value)
+ # Let ActiveRecord handle this
+ klass.send(:sanitize_sql_hash_for_conditions, {column.name => value})
+ end
+ end
+ end
+end
View
25 lib/searchgasm/condition/greater_than.rb
@@ -0,0 +1,25 @@
+module Searchgasm
+ module Condition
+ class GreaterThan < Base
+ class << self
+ def name_for_column(column)
+ return unless comparable_column?(column)
+ super
+ end
+
+ def aliases_for_column(column)
+ column_names = [column.name]
+ column_names << column.name.gsub(/_at$/, "") if [:datetime, :timestamp, :time, :date].include?(column.type) && column.name =~ /_at$/
+
+ aliases = []
+ column_names.each { |column_name| aliases += ["#{column_name}_gt", "#{column_name}_after"] }
+ aliases
+ end
+ end
+
+ def to_conditions(value)
+ ["#{quoted_table_name}.#{quoted_column_name} > ?", value]
+ end
+ end
+ end
+end
View
20 lib/searchgasm/condition/greater_than_or_equal_to.rb
@@ -0,0 +1,20 @@
+module Searchgasm
+ module Condition
+ class GreaterThanOrEqualTo < Base
+ class << self
+ def name_for_column(column)
+ return unless comparable_column?(column)
+ super
+ end
+
+ def aliases_for_column(column)
+ ["#{column.name}_gte", "#{column.name}_at_least"]
+ end
+ end
+
+ def to_conditions(value)
+ ["#{quoted_table_name}.#{quoted_column_name} >= ?", value]
+ end
+ end
+ end
+end
View
13 lib/searchgasm/condition/inclusive_descendant_of.rb
@@ -0,0 +1,13 @@
+module Searchgasm
+ module Condition
+ class InclusiveDescendantOf < Tree
+ include Searchgasm::Utilities
+
+ def to_conditions(value)
+ condition = DescendantOf.new(klass, column)
+ condition.value = value
+ merge_conditions(["#{quoted_table_name}.#{quote_column_name(klass.primary_key)} = ?", (value.is_a?(klass) ? value.send(klass.primary_key) : value)], condition.sanitize, :any => true)
+ end
+ end
+ end
+end
View
33 lib/searchgasm/condition/keywords.rb
@@ -0,0 +1,33 @@
+module Searchgasm
+ module Condition
+ class Keywords < Base
+ BLACKLISTED_WORDS = ('a'..'z').to_a + ["about", "an", "are", "as", "at", "be", "by", "com", "de", "en", "for", "from", "how", "in", "is", "it", "la", "of", "on", "or", "that", "the", "the", "this", "to", "und", "was", "what", "when", "where", "who", "will", "with", "www"] # from ranks.nl
+
+ class << self
+ def name_for_column(column)
+ return unless string_column?(column)
+ super
+ end
+
+ def aliases_for_column(column)
+ ["#{column.name}_kwords", "#{column.name}_kw"]
+ end
+ end
+
+ def to_conditions(value)
+ strs = []
+ subs = []
+
+ search_parts = value.gsub(/,/, " ").split(/ /).collect { |word| word.downcase.gsub(/[^[:alnum:]]/, ''); }.uniq.select { |word| !BLACKLISTED_WORDS.include?(word.downcase) && !word.blank? }
+ return if search_parts.blank?
+
+ search_parts.each do |search_part|
+ strs << "#{quoted_table_name}.#{quoted_column_name} LIKE ?"
+ subs << "%#{search_part}%"
+ end
+
+ [strs.join(" AND "), *subs]
+ end
+ end
+ end
+end
View
25 lib/searchgasm/condition/less_than.rb
@@ -0,0 +1,25 @@
+module Searchgasm
+ module Condition
+ class LessThan < Base
+ class << self
+ def name_for_column(column)
+ return unless comparable_column?(column)
+ super
+ end
+
+ def aliases_for_column(column)
+ column_names = [column.name]
+ column_names << column.name.gsub(/_at$/, "") if [:datetime, :timestamp, :time, :date].include?(column.type) && column.name =~ /_at$/
+
+ aliases = []
+ column_names.each { |column_name| aliases += ["#{column_name}_lt", "#{column_name}_before"] }
+ aliases
+ end
+ end
+
+ def to_conditions(value)
+ ["#{quoted_table_name}.#{quoted_column_name} < ?", value]
+ end
+ end
+ end
+end
View
20 lib/searchgasm/condition/less_than_or_equal_to.rb
@@ -0,0 +1,20 @@
+module Searchgasm
+ module Condition
+ class LessThanOrEqualTo < Base
+ class << self
+ def name_for_column(column)
+ return unless comparable_column?(column)
+ super
+ end
+
+ def aliases_for_column(column)
+ ["#{column.name}_lte", "#{column.name}_at_most"]
+ end
+ end
+
+ def to_conditions(value)
+ ["#{quoted_table_name}.#{quoted_column_name} <= ?", value]
+ end
+ end
+ end
+end
View
16 lib/searchgasm/condition/sibling_of.rb
@@ -0,0 +1,16 @@
+module Searchgasm
+ module Condition
+ class SiblingOf < Tree
+ include Searchgasm::Utilities
+
+ def to_conditions(value)
+ parent_association = klass.reflect_on_association(:parent)
+ foreign_key_name = (parent_association && parent_association.options[:foreign_key]) || "parent_id"
+ parent_id = (value.is_a?(klass) ? value : klass.find(value)).send(foreign_key_name)
+ condition = ChildOf.new(klass, column)
+ condition.value = parent_id
+ merge_conditions(["#{quoted_table_name}.#{quote_column_name(klass.primary_key)} != ?", (value.is_a?(klass) ? value.send(klass.primary_key) : value)], condition.sanitize)
+ end
+ end
+ end
+end
View
16 lib/searchgasm/condition/tree.rb
@@ -0,0 +1,16 @@
+module Searchgasm
+ module Condition
+ class Tree < Base # :nodoc:
+ class << self
+ def name_for_column(column)
+ nil
+ end
+
+ def name_for_klass(klass)
+ return unless klass.reflect_on_association(:parent) && klass.reflect_on_association(:children)
+ condition_name
+ end
+ end
+ end
+ end
+end
View
221 lib/searchgasm/conditions/base.rb
@@ -0,0 +1,221 @@
+module Searchgasm
+ module Conditions # :nodoc:
+ # = Conditions
+ #
+ # Represents a collection of conditions and performs various tasks on that collection. For information on each condition see Searchgasm::Condition.
+ # Each condition has its own file and class and the source for each condition is pretty self explanatory.
+ class Base
+ include Utilities
+
+ attr_accessor :klass, :relationship_name, :scope
+
+ class << self
+ # Registers a condition as an available condition for a column or a class.
+ #
+ # === Example
+ #
+ # config/initializers/searchgasm.rb
+ # # Actual function for MySQL databases only
+ # class SoundsLike < Searchgasm::Condition::Base
+ # class << self
+ # # I pass you the column, you tell me what you want the method to be called.
+ # # If you don't want to add this condition for that column, return nil
+ # # It defaults to "#{column.name}_sounds_like". So if thats what you want you don't even need to do this.
+ # def name_for_column(column)
+ # super
+ # end
+ #
+ # # Only do this if you want aliases for your condition
+ # def aliases_for_column(column)
+ # ["#{column.name}_sounds", "#{column.name}_similar_to"]
+ # end
+ # end
+ #
+ # # You can return an array or a string. NOT a hash, because all of these conditions
+ # # need to eventually get merged together. The array or string can be anything you would put in
+ # # the :conditions option for ActiveRecord::Base.find()
+ # def to_conditions(value)
+ # ["#{quoted_table_name}.#{quoted_column_name} SOUNDS LIKE ?", value]
+ # end
+ # end
+ #
+ # Searchgasm::Seearch::Conditions.register_condition(SoundsLikeCondition)
+ def register_condition(klass)
+ raise(ArgumentError, "You can only register conditions that extend Searchgasm::Condition::Base") unless klass.ancestors.include?(Searchgasm::Condition::Base)
+ conditions << klass unless conditions.include?(klass)
+ end
+
+ # A list of available condition type classes
+ def conditions
+ @@conditions ||= []
+ end
+
+ def needed?(klass, conditions) # :nodoc:
+ if conditions.is_a?(Hash)
+ conditions.stringify_keys.keys.each do |condition|
+ return true unless klass.column_names.include?(condition)
+ end
+ end
+
+ false
+ end
+ end
+
+ def initialize(klass, init_conditions = {})
+ self.klass = klass
+ add_klass_conditions!
+ add_column_conditions!
+ add_associations!
+ self.conditions = init_conditions
+ end
+
+ # Setup methods for searching
+ [:all, :average, :calculate, :count, :find, :first, :maximum, :minimum, :sum].each do |method|
+ class_eval <<-"end_eval", __FILE__, __LINE__
+ def #{method}(*args)
+ self.conditions = args.extract_options!
+ args << {:conditions => sanitize}
+ klass.#{method}(*args)
+ end
+ end_eval
+ end
+
+ # A list of includes to use when searching, includes relationships
+ def includes
+ i = []
+ associations.each do |association|
+ association_includes = association.includes
+ i << (association_includes.blank? ? association.relationship_name.to_sym : {association.relationship_name.to_sym => association_includes})
+ end
+ i.blank? ? nil : (i.size == 1 ? i.first : i)
+ end
+
+ # Sanitizes the conditions down into conditions that ActiveRecord::Base.find can understand.
+ def sanitize
+ conditions = merge_conditions(*objects.collect { |object| object.sanitize })
+ return scope if conditions.blank?
+ merge_conditions(conditions, scope)
+ end
+
+ # Allows you to set the conditions via a hash. If you do not pass a hash it will set scope instead, so that you can continue to add conditions and ultimately
+ # merge it all together at the end.
+ def conditions=(conditions)
+ case conditions
+ when Hash
+ assert_valid_conditions(conditions)
+ remove_conditions_from_protected_assignement(conditions).each { |condition, value| send("#{condition}=", value) }
+ else
+ self.scope = conditions
+ end
+ end
+
+ # All of the active conditions (conditions that have been set)
+ def conditions
+ conditions_hash = {}
+ objects.each do |object|
+ case object
+ when self.class
+ relationship_conditions = object.conditions
+ next if relationship_conditions.blank?
+ conditions_hash[object.relationship_name.to_sym] = relationship_conditions
+ else
+ next unless object.explicitly_set_value?
+ conditions_hash[object.name.to_sym] = object.value
+ end
+ end
+ conditions_hash
+ end
+
+ private
+ def add_associations!
+ klass.reflect_on_all_associations.each do |association|
+ self.class.class_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association.name}
+ if @#{association.name}.nil?
+ @#{association.name} = self.class.new(#{association.class_name})
+ @#{association.name}.relationship_name = "#{association.name}"
+ objects << @#{association.name}
+ end
+ @#{association.name}
+ end
+
+ def #{association.name}=(conditions); #{association.name}.conditions = conditions; end
+ def reset_#{association.name}!; objects.delete(#{association.name}); @#{association.name} = nil; end
+ end_eval
+ end
+ end
+
+ def add_column_conditions!
+ klass.columns.each do |column|
+ self.class.conditions.each do |condition_klass|
+ name = condition_klass.name_for_column(column)
+ next if name.blank?
+ add_condition!(condition_klass, name, column)
+ condition_klass.aliases_for_column(column).each { |alias_name| add_condition_alias!(alias_name, name) }
+ end
+ end
+ end
+
+ def add_condition!(condition, name, column = nil)
+ condition_names << name
+ self.class.class_eval <<-"end_eval", __FILE__, __LINE__
+ def #{name}_object
+ if @#{name}.nil?
+ @#{name} = #{condition.name}.new(klass#{column.nil? ? "" : ", \"#{column.name}\""})
+ objects << @#{name}
+ end
+ @#{name}
+ end
+
+ def #{name}; #{name}_object.value; end
+ def #{name}=(value); #{name}_object.value = value; end
+ def reset_#{name}!; objects.delete(#{name}_object); @#{name} = nil; end
+ end_eval
+ end
+
+ def add_condition_alias!(alias_name, name)
+ condition_names << alias_name
+ self.class.class_eval do
+ alias_method alias_name, name
+ alias_method "#{alias_name}=", "#{name}="
+ end
+ end
+
+ def add_klass_conditions!
+ self.class.conditions.each do |condition|
+ name = condition.name_for_klass(klass)
+ next if name.blank?
+ add_condition!(condition, name)
+ condition.aliases_for_klass(klass).each { |alias_name| add_condition_alias!(alias_name, name) }
+ end
+ end
+
+ def assert_valid_conditions(conditions)
+ keys = condition_names.collect { |condition_name| condition_name.to_sym }
+ keys += klass.reflect_on_all_associations.collect { |association| association.name }
+ conditions.symbolize_keys.assert_valid_keys(keys)
+ end
+
+ def associations
+ objects.select { |object| object.is_a?(self.class) }
+ end
+
+ def condition_names
+ @condition_names ||= []
+ end
+
+ def objects
+ @objects ||= []
+ end
+
+ def remove_conditions_from_protected_assignement(conditions)
+ return conditions if klass.accessible_conditions.nil? && klass.protected_conditions.nil?
+ if klass.accessible_conditions
+ conditions.reject { |condition, value| !klass.accessible_conditions.include?(condition.to_s) }
+ elsif klass.protected_conditions
+ conditions.reject { |condition, value| klass.protected_conditions.include?(condition.to_s) }
+ end
+ end
+ end
+ end
+end
View
30 lib/searchgasm/conditions/protection.rb
@@ -0,0 +1,30 @@
+module Searchgasm
+ module Conditions
+ # = Conditions Protection
+ #
+ # Adds protection from SQL injections. Just set protect = true and it will limit what kind of conditions it will accept.
+ module Protection
+ def self.included(klass)
+ klass.class_eval do
+ attr_accessor :protect
+ alias_method_chain :conditions=, :protection
+ end
+ end
+
+ def conditions_with_protection=(conditions)
+ unless conditions.is_a?(Hash)
+ if protect?
+ return if conditions.blank?
+ raise(ArgumentError, "You can not set a scope or pass SQL while the search is being protected")
+ end
+ end
+
+ self.conditions_without_protection = conditions
+ end
+
+ def protect?
+ protect == true
+ end
+ end
+ end
+end
View
137 lib/searchgasm/config.rb
@@ -0,0 +1,137 @@
+module Searchgasm
+ # = Config
+ # Adds default configuration for all of searchgasm. Just make sure you set your config before you use Searchgasm.
+ # For rails the best place to do this is in config/initializers. Create a file in there called searchgasm.rb with the following content:
+ #
+ # === Example
+ #
+ # # config/iniitializers/searchgasm.rb
+ # Searchgasm::Config.configure do |config|
+ # config.you_option_here = your_value # see methods below
+ # end
+ class Config
+ class << self
+ # Convenience method for setting configuration
+ # See example at top of class.
+ def configure
+ yield self
+ end
+
+ def asc_indicator # :nodoc:
+ @asc_indicator ||= "&nbsp;&#9650;"
+ end
+
+ # The indicator that is used when the sort of a column is ascending
+ #
+ # * <tt>Default:</tt> &nbsp;&#9650;
+ # * <tt>Accepts:</tt> String or a Proc.
+ #
+ # === Examples
+ #
+ # config.asc_indicator = "(ASC)"
+ # config.asc_indicator = Proc.new { |template| template.image_tag("asc.jpg") }
+ def asc_indicator=(value)
+ @asc_indicator = value
+ end
+
+ def desc_indicator # :nodoc:
+ @desc_indicator ||= "&nbsp;&#9660;"
+ end
+
+ # See asc_indicator=
+ def desc_indicator=(value)
+ @desc_indicator = value
+ end
+
+ def pages_type # :nodoc:
+ @pages_type ||= :select
+ end
+
+ # The default value for the :type option in the pages helper.
+ #
+ # * <tt>Default:</tt> :select
+ # * <tt>Accepts:</tt> :select, :links
+ def pages_type=(value)
+ @pages_type = value.to_sym
+ end
+
+ def per_page_choices # :nodoc:
+ @per_page_choices ||= [10, 25, 50, 100, 150, 200, nil]
+ end
+
+ # The choices used in the per_page helper
+ #
+ # * <tt>Default:</tt> [10, 25, 50, 100, 150, 200, nil]
+ # * <tt>Accepts:</tt> Array
+ #
+ # nil means "Show all"
+ def per_page_choices=(value)
+ @per_page_choices = value
+ end
+
+ def per_page_type # :nodoc:
+ @per_page_type ||= :select
+ end
+
+ # The default value for the :type option in the per_page helper.
+ #
+ # * <tt>Default:</tt> :select
+ # * <tt>Accepts:</tt> :select, :links
+ def per_page_type=(value)
+ @per_page_type = value.to_sym
+ end
+
+ def hidden_fields # :nodoc:
+ @hidden_fields ||= (Search::Base::SPECIAL_FIND_OPTIONS - [:page])
+ end
+
+ # Which hidden fields to automatically include when creating a form with a Searchgasm object. See Searchgasm::Helpers::FormHelper for more info.
+ #
+ # * <tt>Default:</tt> [:order_by, :order_as, :per_page]
+ # * <tt>Accepts:</tt> Array, nil, false
+ def hidden_fields=(value)
+ @hidden_fields = value
+ end
+
+ def remote_helpers # :nodoc:
+ @remote_helpers ||= false
+ end
+
+ # Tells all helpers to default to using remote links (AJAX) instead of normal links.
+ #
+ # * <tt>Default:</tt> false
+ # * <tt>Accepts:</tt> Boolean
+ #
+ # nil means "Show all"
+ def remote_helpers=(value)
+ @remote_helpers = value
+ end
+
+ def remote_helpers? # :nodoc:
+ remote_helpers == true
+ end
+
+ def search_scope # :nodoc:
+
+ end
+
+ def search_obj_name # :nodoc:
+ @search_obj_name ||= :@search
+ end
+
+ # The instance variable name you use to assign your search to. This allows the helpers to grab your Searchgasm object without having
+ # to specify it everywhere.
+ #
+ # * <tt>Default:</tt> :@search
+ # * <tt>Accepts:</tt> String or Symbol.
+ #
+ # === Examples
+ #
+ # config.search_obj_name = :@search
+ # config.search_obj_name = "@search"
+ def search_obj_name=(value)
+ @search_obj_name = value
+ end
+ end
+ end
+end
View
139 lib/searchgasm/helpers/form_helper.rb
@@ -1,78 +1,103 @@
module Searchgasm
module Helpers
+ # = Form Helper
+ #
+ # Enables you to use form_for and fields_for just like you do with an ActiveRecord object.
+ #
+ # === Examples
+ #
+ # Let's assume @search is searching Address
+ #
+ # form_for(@search) # is equivalent to form_for(:search, @search, :url => addresses_path)
+ # form_for([@current_user, @search]) # is equivalent to form_for(:search, @search, :url => user_addresses_path(@current_user))
+ # form_for([:admin, @search]) # is equivalent to form_for(:search, @search, :url => admin_addresses_path)
+ # form_for(:search, @search, :url => whatever_path)
+ #
+ # The goal was to mimic ActiveRecord. You can also pass a Searchgasm::Conditions::Base object as well and it will function the same way.
+ #
+ # === Automatic hidden fields generation
+ #
+ # If you pass a Searchgasm::Search::Base object it automatically adds the :order_by, :order_as, and :per_page hidden fields. This is done so that when someone
+ # creates a new search, their options are remembered. It keeps the search consisten and is much more user friendly. If you want to override this you can pass the
+ # following options. Or you can set this up in your configuration, see Searchgasm::Config for more details.
+ #
+ # === Options
+ #
+ # * <tt>:hidden_fields</tt> --- Array, a list of hidden fields to include. Defaults to [:order_by, :order_as, :per_page]. Pass false, nil, or a blank array to not include any.
module FormHelper
- module Shared
-
- def searchgasm_object?(object)
- object.is_a?(Search::Base) || object.is_a?(Search::Conditions)
- end
-
- def find_searchgasm_object(args)
- case args.first
- when String, Symbol
- search_object = searchgasm_object?(args[1]) ? args[1] : instance_variable_get("@#{args.first}")
- when Array
- search_object = args.first.last
- else
- search_object = args.first
+ module Shared # :nodoc:
+ private
+ def searchgasm_object?(object)
+ object.is_a?(Search::Base) || object.is_a?(Conditions::Base)
end
-
- searchgasm_object?(search_object) ? search_object : nil
- end
- def searchgasm_args(args, search_object, fields_for = false)
- args = args.dup
- first = args.shift
+ def find_searchgasm_object(args)
+ case args.first
+ when String, Symbol
+ search_object = searchgasm_object?(args[1]) ? args[1] : instance_variable_get("@#{args.first}")
+ when Array
+ search_object = args.first.last
+ else
+ search_object = args.first
+ end
- # Setup args
- case first
- when String, Symbol
- args.unshift(search_object).unshift(first)
- else
- name = search_object.is_a?(Search::Conditions) ? (search_object.relationship_name || :conditions) : :search
- args.unshift(search_object).unshift(name)
+ searchgasm_object?(search_object) ? search_object : nil
end
+
+ def searchgasm_args(args, search_object, for_helper = nil)
+ args = args.dup
+ first = args.shift
- if !fields_for
- options = args.extract_options!
- options[:html] ||= {}
- options[:html][:method] ||= :get
- #options[:html][:id] ||= searchgasm_form_id(search_object)
-
- # Setup options
+ # Setup args
case first
- when Array
- first.pop
- first << search_object.klass.new
- options[:url] ||= polymorphic_path(first)
+ when String, Symbol
+ args.unshift(search_object).unshift(first)
else
- options[:url] ||= polymorphic_path(search_object.klass.new)
+ name = search_object.is_a?(Conditions::Base) ? (search_object.relationship_name || :conditions) : :search
+ args.unshift(search_object).unshift(name)
end
- args << options
- end
+ if for_helper != :fields_for
+ options = args.extract_options!
+ options[:html] ||= {}
+ options[:html][:method] ||= :get
+ options[:method] ||= options[:html][:method] if for_helper == :remote_form_for
+ options[:html][:id] ||= searchgasm_form_id(search_object)
- args
- end
+ # Setup options
+ case first
+ when Array
+ first.pop
+ first << search_object.klass.new
+ options[:url] ||= polymorphic_path(first)
+ else
+ options[:url] ||= polymorphic_path(search_object.klass.new)
+ end
+
+ args << options
+ end
+
+ args
+ end
- def insert_searchgasm_fields(args, search_object)
- return unless search_object.is_a?(Search::Base)
- name = args.first
- options = args.extract_options!
- (Search::Base::SPECIAL_FIND_OPTIONS - [:page]).each do |option|
- concat(hidden_field(name, option, :object => search_object)) unless options.delete(option) == false
+ def insert_searchgasm_fields(args, search_object)
+ return unless search_object.is_a?(Search::Base)
+ name = args.first
+ options = args.extract_options!
+ (options.delete(:hidden_fields) || Config.hidden_fields).each do |option|
+ concat(hidden_field(name, option, :object => search_object, :value => (option == :order_by ? searchgasm_order_by_value(search_object.order_by) : search_object.send(option))))
+ end
+ args << options
end
- args << options
- end
end
- module Base
+ module Base # :nodoc:
include Shared
def fields_for_with_searchgasm(*args, &block)
search_object = find_searchgasm_object(args)
if search_object
- new_args = searchgasm_args(args, search_object, true)
+ new_args = searchgasm_args(args, search_object, :fields_for)
insert_searchgasm_fields(new_args, search_object)
fields_for_without_searchgasm(*new_args, &block)
else
@@ -83,7 +108,7 @@ def fields_for_with_searchgasm(*args, &block)
def form_for_with_searchgasm(*args, &block)
search_object = find_searchgasm_object(args)
if search_object
- form_for_without_searchgasm(*searchgasm_args(args, search_object), &block)
+ form_for_without_searchgasm(*searchgasm_args(args, search_object, :form_for), &block)
else
form_for_without_searchgasm(*args, &block)
end
@@ -92,20 +117,20 @@ def form_for_with_searchgasm(*args, &block)
def remote_form_for_with_searchgasm(*args, &block)
search_object = find_searchgasm_object(args)
if search_object
- remote_form_for_without_searchgasm(*searchgasm_args(args, search_object), &block)
+ remote_form_for_without_searchgasm(*searchgasm_args(args, search_object, :remote_form_for), &block)
else
remote_form_for_without_searchgasm(*args, &block)
end
end
end
- module FormBuilder
+ module FormBuilder # :nodoc:
include Shared
def fields_for_with_searchgasm(*args, &block)
search_object = find_searchgasm_object(args)
if search_object
- new_args = searchgasm_args(args, search_object, true)
+ new_args = searchgasm_args(args, search_object, :fields_for)
insert_searchgasm_fields(new_args, search_object)
fields_for_without_searchgasm(*new_args, &block)
else
View
37 lib/searchgasm/helpers/order_helper.rb
@@ -1,37 +0,0 @@
-module Searchgasm
- module Helpers
- module OrderHelper
- # Creates a link for ordering data in a certain way.
- #
- # === Example uses for a User class that has many orders
- # order_by(:first_name)
- # order_by([:first_name, :last_name])
- # order_by({:orders => :total})
- # order_bt([{:orders => :total}, :first_name])
- #
- # The value gets "serialized" so that it can be passed via a param in the url. Searchgasm will automatically "unserializes" this value and uses it.
- #
- # === Options
- # Global options:
- # Please see Searchgasm::Helpers::Utilities.add_searchgasm_helper_defaults for all global options
- #
- # Local options:
- # * <tt>:text</tt> -- default: column_name.to_s.humanize, text for the link
- # * <tt>:desc_indicator</tt> -- default: &nbsp;&#9660;, the indicator that this column is descending
- # * <tt>:asc_indicator</tt> -- default: &nbsp;&#9650;, the indicator that this column is ascending
- # * <tt>:remote</tt> -- default: false, if true requests will be AJAX
- # * <tt>:html</tr> -- html_options for the link function
- def order_by(column_name, options = {})
- column_name = column_name.to_s
- add_searchgasm_helper_defaults!(options, :order_by)
- options[:text] ||= column_name.humanize
- options[:asc_indicator] ||= "&nbsp;&#9650;"
- options[:desc_indicator] ||= "&nbsp;&#9660;"
- options[:text] += options[:search].desc? ? options[:desc_indicator] : options[:asc_indicator] if options[:search].order_by == column_name
- link_to(options[:text], options[:url], options[:html])
- end
- end
- end
-end
-
-ActionController::Base.helper Searchgasm::Helpers::OrderHelper if defined?(ActionController)
View
69 lib/searchgasm/helpers/pagination_helper.rb
@@ -1,69 +0,0 @@
-module Searchgasm
- module Helpers
- module PaginationHelper
- # Creates navigation for paginating through a search
- #
- # === Options
- # Global options:
- # Please see Searchgasm::Helpers::Utilities.add_searchgasm_helper_defaults for all global options
- #
- # Local options:
- # * <tt>:type</tt> -- default: :select, pass :links as an alternative to have flickr like pagination
- # * <tt>:remote</tt> -- default: false, if true requests will be AJAX
- # * <tt>:html</tr> -- if :links then these will apply to the outermost <div>, if :select then these will apply to the select tag
- def pages(options = {})
- options[:type] ||= :select
-
- case options[:type]
- when :select
- options[:javascript] = true
- else
- end
-
- add_searchgasm_helper_defaults!(options, :page)
- return "" if options[:search].page_count <= 1
-
- case options[:type]
- when :select
- options[:html] ||= {}
- options[:html][:onchange] ||= ""
- options[:html][:onchange] += ";" + options[:url]
- select(:search, :page, (1..options[:search].page_count), {}, options[:html])
- else
- # HTML for links
- end
- end
-
- # Creates navigation for setting how many items per page
- #
- # === Options
- # Global options:
- # Please see Searchgasm::Helpers::Utilities.add_searchgasm_helper_defaults for all global options
- #
- # Local options:
- # * <tt>:type</tt> -- default: :select, pass :links as an alternative to links like: 10 | 25 | 50 | 100, etc
- # * <tt>:remote</tt> -- default: false, if true requests will be AJAX
- # * <tt>:choices</tt> -- default: [10, 25, 50, 100, 150, 200, nil], nil means "show all"
- # * <tt>:html</tr> -- if :links then these will apply to the outermost <div>, if :select then these will apply to the select tag
- def per_page(options = {})
- options[:javascript] = true
- add_searchgasm_helper_defaults!(options, :per_page)
-
- options[:choices] = [10, 25, 50, 100, 150, 200, nil]
- if !options[:search].per_page.blank? && !options[:choices].include?(options[:search].per_page)
- options[:choices] << options[:search].per_page
- options[:choices].sort!
- end
- options[:choices] = options[:choices].collect { |choice| [choice == nil ? "Show all" : "#{choice} per page", choice] }
-
- #link_to "page 2", url_for(:search => {:page => 2, :per_page => 5, :conditions => options[:search].conditions(true)})
- options[:html] ||= {}
- options[:html][:onchange] ||= options[:url]
-
- select(:search, :per_page, options[:choices], {}, options[:html])
- end
- end
- end
-end
-
-ActionController::Base.helper Searchgasm::Helpers::PaginationHelper if defined?(ActionController)
View
178 lib/searchgasm/helpers/search_helper.rb
@@ -0,0 +1,178 @@
+module Searchgasm
+ module Helpers
+ # = Search Helper
+ #
+ # Helper methods for paginating and ordering through a search.
+ module SearchHelper
+ # Creates a link for ordering data in a certain way. See Searchgasm::Config for setting default configuration.
+ #
+ # === Example uses for a User class that has many orders
+ # order_by(:first_name)
+ # order_by([:first_name, :last_name])
+ # order_by({:orders => :total})
+ # order_bt([{:orders => :total}, :first_name])
+ #
+ # If the output just isn't cutting it for you, then you can pass it a block and it will spit out the result of the block. The block is passed "options" which is all of the information you should need
+ # for doing whatever you need to do. It's the options you are allowed to pass, but with their proper values.
+ #
+ # <%= order_by(:id) { |options| link_to(options[:text], options[:url], options[:html]) } %>
+ #
+ # or
+ #
+ # <% order_by(:id) do |options| %><%= link_to(options[:text], options[:url]) %><% end %>
+ #
+ # Another thing to keep in mind is that the value gets "serialized", if it is not a string or a simple, so that it can be passed via a param in the url. Searchgasm will automatically try to "unserializes" this value and use it. This allows you
+ # to pass complex objects besides strings and symbols, such as arrays and hashes.
+ #
+ # === Options
+ # * <tt>:text</tt> -- default: column_name.to_s.humanize, text for the link
+ # * <tt>:desc_indicator</tt> -- default: &nbsp;&#9660;, the indicator that this column is descending
+ # * <tt>:asc_indicator</tt> -- default: &nbsp;&#9650;, the indicator that this column is ascending
+ #
+ # === Advanced Options
+ # * <tt>:action</tt> -- this is automatically determined for you based on the type. For a :select type, its :onchange. For a :links type, its :onclick. You shouldn't have to use this option unless you are doing something out of the norm. The point of this option is to return a URL that will include this.value or not
+ # * <tt>:url</tt> -- default: uses url_for to preserve your params and search, I can not think of a reason to pass this, but its there just incase
+ # * <tt>:html</tt> -- if type is :links then these will apply to the outermost <div>, if type is :select then these will apply to the select tag
+ # * <tt>:search_obj</tt> -- default: @search, this is your search object, if it is in an instance variable other than @search please pass it here. Ex: :@my_search, or :my_search
+ # * <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.
+ def order_by(column_name, options = {}, &block)
+ add_searchgasm_helper_defaults!(options, :order_by, column_name)
+ column_name = stringify_everything(column_name)
+ options[:text] = determine_order_by_text(column_name) unless options.has_key?(:text)
+ options[:asc_indicator] ||= Config.asc_indicator
+ options[:desc_indicator] ||= Config.desc_indicator
+ options[:text] += options[:search_obj].desc? ? options[:desc_indicator] : options[:asc_indicator] if options[:search_obj].order_by == column_name
+
+ if block_given?
+ yield options
+ else
+ if options[:remote]
+ link_to_function(options[:text], options[:url], options[:html])
+ else
+ link_to(options[:text], options[:url], options[:html])
+ end
+ end
+ end
+
+ # Creates navigation for paginating through a search. See Searchgasm::Config for setting default configuration.
+ #
+ # === Examples
+ # pages
+ # pages(:search => @my_search)
+ # pages(:html => {:id => "my_id"})
+ #
+ # If the output just isn't cutting it for you, then you can pass it a block and it will spit out the result of the block. The block is passed "options" which is all of the information you should need
+ # for doing whatever you need to do. It's the options you are allowed to pass, but with their proper values.
+ #
+ # <%= pages { |options| select(:search, :page, (1..options[:search_obj].page_count), {}, options[:html]) } %>
+ #
+ # or
+ #
+ # <% pages do |options| %><%= select(:search, :page, (1..options[:search_obj].page_count), {}, options[:html]) %><% end %>
+ #
+ # === Options
+ # * <tt>:type</tt> -- default: :select, pass :links as an alternative to have flickr like pagination
+ # * <tt>:remote</tt> -- default: false, if true requests will be AJAX
+ #
+ # === Advanced Options
+ # * <tt>:action</tt> -- this is automatically determined for you based on the type. For a :select type, its :onchange. For a :links type, its :onclick. You shouldn't have to use this option unless you are doing something out of the norm. The point of this option is to return a URL that will include this.value or not
+ # * <tt>:url</tt> -- default: uses url_for to preserve your params and search, I can not think of a reason to pass this, but its there just incase
+ # * <tt>:html</tt> -- if type is :links then these will apply to the outermost <div>, if type is :select then these will apply to the select tag
+ # * <tt>:search_obj</tt> -- default: @search, this is your search object, if it is in an instance variable other than @search please pass it here. Ex: :@my_search, or :my_search
+ # * <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.
+ def pages(options = {})
+ options[:type] ||= Config.pages_type
+ add_searchgasm_helper_defaults!(options, :page)
+ return "" if options[:search_obj].page_count <= 1
+
+ if block_given?
+ yield options
+ else
+ case options[:type]
+ when :select
+ options[:html] ||= {}
+ options[:html][options[:action]] ||= ""
+ options[:html][options[:action]] += ";"
+ options[:html][options[:action]] += options[:url]
+ select(:search, :page, (1..options[:search_obj].page_count), {}, options[:html])
+ else
+ # HTML for links
+ end
+ end
+ end
+
+ # Creates navigation for setting how many items per page. See Searchgasm::Config for setting default configuration.
+ #
+ # === Examples
+ # per_page
+ # per_page(:search => @my_search)
+ # per_page(:choices => [50, 100])
+ # per_page(:html => {:id => "my_id"})
+ #
+ # If the output just isn't cutting it for you, then you can pass it a block and it will spit out the result of the block. The block is passed "options" which is all of the information you should need
+ # for doing whatever you need to do. It's the options you are allowed to pass, but with their proper values.
+ #
+ # <%= per_page { |options| select(:search, :per_page, options[:choices], {}, options[:html]) } %>
+ #
+ # or
+ #
+ # <% per_page do |options| %><%= select(:search, :per_page, options[:choices], {}, options[:html]) %><% end %>
+ #
+ # === Options
+ # * <tt>:type</tt> -- default: :select, pass :links as an alternative to links like: 10 | 25 | 50 | 100, etc
+ # * <tt>:remote</tt> -- default: false, if true requests will be AJAX
+ # * <tt>:choices</tt> -- default: [10, 25, 50, 100, 150, 200, nil], nil means "show all"
+ #
+ # === Advanced Options
+ # * <tt>:choices</tt> -- default: [10, 25, 50, 100, 150, 200, nil], nil means "show all"
+ # * <tt>:action</tt> -- this is automatically determined for you based on the type. For a :select type, its :onchange. For a :links type, its :onclick. You shouldn't have to use this option unless you are doing something out of the norm. The point of this option is to return a URL that will include this.value or not
+ # * <tt>:url</tt> -- default: uses url_for to preserve your params and search, I can not think of a reason to pass this, but its there just incase
+ # * <tt>:html</tt> -- if type is :links then these will apply to the outermost <div>, if type is :select then these will apply to the select tag
+ # * <tt>:search_obj</tt> -- default: @search, this is your search object, if it is in an instance variable other than @search please pass it here. Ex: :@my_search, or :my_search
+ # * <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.
+ def per_page(options = {})
+ options[:type] ||= Config.per_page_type
+ add_searchgasm_helper_defaults!(options, :per_page)
+
+ options[:choices] ||= Config.per_page_choices
+ if !options[:search_obj].per_page.blank? && !options[:choices].include?(options[:search_obj].per_page)
+ options[:choices] << options[:search_obj].per_page
+ has_nil = options[:choices].include?(nil)
+ options[:choices].delete(nil) if has_nil
+ options[:choices].sort!
+ options[:choices] << nil if has_nil
+ end
+ options[:choices] = options[:choices].collect { |choice| [choice == nil ? "Show all" : "#{choice} per page", choice] }
+
+ if block_given?
+ yield options
+ else
+ case options[:type]
+ when :select
+ options[:html] ||= {}
+ options[:html][options[:action]] ||= ""
+ options[:html][options[:action]] += ";"
+ options[:html][options[:action]] += options[:url]
+ select(:search, :per_page, options[:choices], {}, options[:html])
+ end
+ end
+ end
+
+ private
+ def determine_order_by_text(column_name, relationship_name = nil)
+ case column_name
+ when String, Symbol
+ relationship_name.blank? ? column_name.titleize : "#{relationship_name.titleize} #{column_name.titleize}"
+ when Array
+ determine_order_by_text(column_name.last)
+ when Hash
+ k = column_name.keys.first
+ v = column_name.values.first
+ determine_order_by_text(v, k)
+ end
+ end
+ end
+ end
+end
+
+ActionController::Base.helper Searchgasm::Helpers::SearchHelper if defined?(ActionController)
View
145 lib/searchgasm/helpers/utilities_helper.rb
@@ -1,42 +1,123 @@
module Searchgasm
- module Helpers
- module UtilitiesHelper
- # Requires that a search object be present. Either by explicitly passing it with the :search option or finding it view the @search instance variable
- def require_search(options = {}) #:nodoc:
- search = options[:search] || instance_variable_get(:@search)
- raise(ArgumentError, "@search object could not be inferred, please specify: order_by(:first_name, :search => @search)") unless search.is_a?(Searchgasm::Search::Base)
- search
- end
-
- # Adds default options for all helper methods.
- #
- # === Global Options
- # * <tt>:search</th> -- default: @search, this is your search object, if it is in an instance variable other than @search please pass it here
- # * <tt>:search_scope</th> -- default: :search, this is the scope in which your search params are held (params[:search]). This is required to preserve the search params.
- def add_searchgasm_helper_defaults!(options, method)
- options[:search] ||= instance_variable_get(:@search)
- raise(ArgumentError, "@search object could not be inferred, please specify: order_by(:first_name, :search => @search)") unless options[:search].is_a?(Searchgasm::Search::Base)
-
- options[:search_scope] ||= :search
+ module Helpers #:nodoc:
+ module UtilitiesHelper # :nodoc:
+ private
+ # Adds default options for all helper methods.
+ def add_searchgasm_helper_defaults!(options, method_name, method_value = nil)
+ options[:search_obj] ||= instance_variable_get(Config.search_obj_name)
+ raise(ArgumentError, "@search object could not be inferred, please specify: :search_obj => @search)") unless options[:search_obj].is_a?(Searchgasm::Search::Base)
+ method_value = stringify_everything(method_value) unless method_value.nil?
+ options[:params_scope] = :search unless options.has_key?(:params_scope)
+ options[:remote] = Config.remote_helpers? unless options.has_key?(:remote)
+
+ if !options.has_key?(:action)
+ if options[:type] == :select
+ options[:action] = :onchange
+ elsif options[:remote]
+ options[:action] = :onclick
+ end
+ end
+
+ options[:url] = searchgasm_url(options, method_name, method_value) unless options.has_key?(:url)
+
+ options
+ end
- if options[:url].blank?
- p = (params || {}).dup
- if p[options[:search_scope]]
- p[options[:search_scope]].delete(method)
- p[options[:search_scope]].delete(:page) if [:per_page, :order_by].include?(method)
+ def searchgasm_url(options, method_name, method_value = nil)
+ params = (params || {}).dup
+ params.delete(:commit)
+
+ # Extract search params from params
+ search_params = options[:params_scope].blank? ? params : params[options[:params_scope]] ||= {}
+
+ # Rewrite :order_by and :per_page with what's in our search obj
+ ([:order_by, :per_page] - [method_name]).each { |search_option| search_params[search_option] = options[:search_obj].send(search_option) }
+
+ # Rewrite :conditions, separated due to unique call
+ conditions = options[:search_obj].conditions.conditions
+ search_params[:conditions] = conditions unless conditions.blank?
+
+ # Never want to keep page or the option we are trying to set
+ [:page, method_name].each { |option| search_params.delete(option) }
+
+ # Alternate :order_by if we are ordering
+ if method_name == :order_by
+ search_params[:order_as] = (options[:search_obj].order_by == method_value && options[:search_obj].asc?) ? "DESC" : "ASC"
+ else
+ search_params[:order_as] = options[:search_obj].order_as
end
- if options[:javascript]
- url = url_for(p)
- options[:url] = "window.location = '" + url + (url.last == "?" ? "" : (url.include?("?") ? "&amp;" : "?")) + "#{CGI.escape("#{options[:search_scope]}[#{method}]")}='+this.value;"
+ # Determine if this.value should be included or not, and set up url
+ url = nil
+ case options[:action]
+ when :onchange
+ # Include this.value
+ url = url_for(params)
+ url_option = CGI.escape((options[:params_scope].blank? ? "#{method_name}" : "#{options[:params_scope]}[#{method_name}]")) + "='+this.value"
+ url += (url.last == "?" ? "" : (url.include?("?") ? "&amp;" : "?")) + url_option
else
- p[options[:search_scope]] ||= {}
- p[options[:search_scope]][method] = options[method]
- p[options[:search_scope]][:order_as] = options[:search].desc? ? "ASC" : "DESC" if method == :order_by
- options[:url] = url_for(p)
+ # Build the plain URL
+ search_params[method_name] = method_name == :order_by ? searchgasm_order_by_value(method_value) : method_value
+ url = url_for(params)
+ end
+
+ # Now update options if remote
+ if options[:remote]
+ url = remote_function(:url => url, :method => :get).gsub(/\\'\+this.value'/, "'+this.value") + ";"
+
+ update_fields = {method_name => method_value}
+ update_fields[:order_as] = search_params[:order_as] if method_name == :order_by
+ update_fields.each { |field, value| url += ";" + searchgasm_update_search_field_javascript(field, value, options) }
+ elsif !options[:action].blank?
+ # Add some javascript if its onclick
+ url = "window.location = '" + url + ";"
+ end
+
+ url
+ end
+
+ def searchgasm_update_search_field_javascript(field, value, options)
+ field_value = nil
+
+ case options[:action]
+ when :onchange
+ field_value = "this.value";
+ else
+ field_value = field == :order_by ? searchgasm_order_by_value(value) : value
+ field_value = "'#{CGI.escape(field_value)}'"
+ end
+
+ field_name = options[:params_scope] ? "#{options[:params_scope]}[#{field}]" : "#{field}"
+ "#{field} = $('#{searchgasm_form_id(options[:search_obj])}').getInputs('hidden', '#{field_name}'); if(#{field}.length > 0) { #{field}[0].value = #{field_value}; }"
+ end
+
+ def searchgasm_form_id(search_obj)
+ "#{search_obj.klass.name.pluralize.underscore}_search_form"
+ end
+
+ def searchgasm_order_by_value(order_by)
+ case order_by
+ when String
+ order_by
+ when Array, Hash
+ [Marshal.dump(order_by)].pack("m")
+ end
+ end
+
+ def stringify_everything(obj)
+ case obj
+ when String
+ obj
+ when Symbol
+ obj = obj.to_s
+ when Array
+ obj = obj.collect { |item| stringify_everything(item) }
+ when Hash
+ new_obj = {}
+ obj.each { |key, value| new_obj[key.to_s] = stringify_everything(value) }
+ new_obj
end
end
- end
end
end
end
View
263 lib/searchgasm/search/base.rb
@@ -1,44 +1,41 @@
-module Searchgasm
- module Search
+module Searchgasm #:nodoc:
+ module Search #:nodoc:
+ # = Searchgasm
+ #
+ # Please refer the README.rdoc for usage, examples, and installation.
+
class Base
- include Utilities
+ include Searchgasm::Utilities