module Searchgasm
module ActiveRecord
# = Searchgasm ActiveRecord Base
# Adds in base level functionality to ActiveRecord
module Base
# This is an alias method chain. It hook into ActiveRecord's "calculate" method and checks to see if Searchgasm should get involved.
def calculate_with_searchgasm(*args)
options = args.extract_options!
options = sanitize_options_with_searchgasm(options)
args << options
calculate_without_searchgasm(*args)
end
# This is an alias method chain. It hooks into ActiveRecord's "find" method and checks to see if Searchgasm should get involved.
def find_with_searchgasm(*args)
options = args.extract_options!
options = sanitize_options_with_searchgasm(options)
args << options
find_without_searchgasm(*args)
end
# This is an alias method chain. It hooks into ActiveRecord's scopes and checks to see if Searchgasm should get involved. Allowing you to use all of Searchgasms conditions and tools
# in scopes as well.
#
# === Examples
#
# Named scopes:
#
# named_scope :top_expensive, :conditions => {:total_gt => 1_000_000}, :per_page => 10
# named_scope :top_expensive_ordered, :conditions => {:total_gt => 1_000_000}, :per_page => 10, :order_by => {:user => :first_name}
#
# Good ole' regular scopes:
#
# with_scope(:find => {:conditions => {:total_gt => 1_000_000}, :per_page => 10}) do
# find(:all)
# end
#
# with_scope(:find => {:conditions => {:total_gt => 1_000_000}, :per_page => 10}) do
# build_search
# end
def with_scope_with_searchgasm(method_scoping = {}, action = :merge, &block)
method_scoping[:find] = sanitize_options_with_searchgasm(method_scoping[:find]) if method_scoping[:find]
with_scope_without_searchgasm(method_scoping, action, &block)
end
# This is a special method that Searchgasm adds in. It returns a new conditions object on the model. So you can search by conditions *only*.
#
# This method is "protected". Meaning it checks the passed options for SQL injections. So trying to write raw SQL in *any* of the option will result in a raised exception. It's safe to pass a params object when instantiating.
#
# === Examples
#
# conditions = User.new_conditions
# conditions.first_name_contains = "Ben"
# conditions.all # can call any search method: first, find(:all), find(:first), sum("id"), etc...
def build_conditions(values = {}, &block)
conditions = searchgasm_conditions
conditions.protect = true
conditions.conditions = values
yield conditions if block_given?
conditions
end
# See build_conditions. This is the same method but *without* protection. Do *NOT* pass in a params object to this method.
def build_conditions!(values = {}, &block)
conditions = searchgasm_conditions(values)
yield conditions if block_given?
conditions
end
# This is a special method that Searchgasm adds in. It returns a new search object on the model. So you can search via an object.
#
# This method is "protected". Meaning it checks the passed options for SQL injections. So trying to write raw SQL in *any* of the option will result in a raised exception. It's safe to pass a params object when instantiating.
#
# This method has an alias "new_search"
#
# === Examples
#
# search = User.new_search
# search.conditions.first_name_contains = "Ben"
# search.per_page = 20
# search.page = 2
# search.order_by = {:user_group => :name}
# search.all # can call any search method: first, find(:all), find(:first), sum("id"), etc...
def build_search(options = {}, &block)
search = searchgasm_searcher
search.protect = true
search.options = options
yield search if block_given?
search
end
# See build_search. This is the same method but *without* protection. Do *NOT* pass in a params object to this method.
#
# This also has an alias "new_search!"
def build_search!(options = {}, &block)
search = searchgasm_searcher(options)
yield search if block_given?
search
end
# Similar to ActiveRecord's attr_protected, but for conditions. It will block any conditions in this array that are being mass assigned. Mass assignments are:
#
# === Examples
#
# search = User.new_search(:conditions => {:first_name_like => "Ben", :email_contains => "binarylogic.com"})
# search.options = {:conditions => {:first_name_like => "Ben", :email_contains => "binarylogic.com"}}
#
# If first_name_like is in the list of conditions_protected then it will be removed from the hash.
def conditions_protected(*conditions)
write_inheritable_attribute(:conditions_protected, Set.new(conditions.map(&:to_s)) + (protected_conditions || []))
end
def protected_conditions # :nodoc:
read_inheritable_attribute(:conditions_protected)
end
# This is the reverse of conditions_protected. You can specify conditions here and *only* these conditions will be allowed in mass assignment. Any condition not specified here will be blocked.
def conditions_accessible(*conditions)
write_inheritable_attribute(:conditions_accessible, Set.new(conditions.map(&:to_s)) + (protected_conditions || []))
end
def accessible_conditions # :nodoc:
read_inheritable_attribute(:conditions_accessible)
end
private
def sanitize_options_with_searchgasm(options = {})
return options unless Searchgasm::Search::Base.needed?(self, options)
search = Searchgasm::Search::Base.create_virtual_class(self).new(options) # call explicitly to avoid merging the scopes into the searcher
search.acting_as_filter = true
search.sanitize
end
def searchgasm_conditions(options = {})
conditions = Searchgasm::Conditions::Base.create_virtual_class(self).new(options)
conditions.conditions = (scope(:find) || {})[:conditions]
conditions
end
def searchgasm_searcher(options = {})
search = Searchgasm::Search::Base.create_virtual_class(self).new(options)
options_from_scope_for_searchgasm(options).each { |option, value| search.send("#{option}=", value) }
search
end
def options_from_scope_for_searchgasm(options)
# The goal here is to mimic how scope work. Merge what scopes would and don't what they wouldn't.
scope = scope(:find) || {}
scope_options = {}
[:group, :include, :select, :readonly].each { |option| scope_options[option] = scope[option] if !options.has_key?(option) && scope.has_key?(option) }
if scope[:joins] || options[:joins]
scope_options[:joins] = []
scope_options[:joins] += scope[:joins].is_a?(Array) ? scope[:joins] : [scope[:joins]] unless scope[:joins].blank?
scope_options[:joins] += options[:joins].is_a?(Array) ? options[:joins] : [options[:joins]] unless options[:joins].blank?
end
scope_options[:limit] = scope[:limit] if !options.has_key?(:per_page) && !options.has_key?(:limit) && scope.has_key?(:per_page)
scope_options[:offset] = scope[:offset] if !options.has_key?(:page) && !options.has_key?(:offset) && scope.has_key?(:offset)
scope_options[:order] = scope[:order] if !options.has_key?(:order_by) && !options.has_key?(:order) && scope.has_key?(:order)
scope_options[:conditions] = scope[:conditions] if scope.has_key?(:conditions)
scope_options
end
end
end
end
ActiveRecord::Base.send(:extend, Searchgasm::ActiveRecord::Base)
module ActiveRecord #:nodoc: all
class Base
class << self
alias_method_chain :calculate, :searchgasm
alias_method_chain :find, :searchgasm
alias_method_chain :with_scope, :searchgasm
alias_method :new_conditions, :build_conditions
alias_method :new_conditions!, :build_conditions!
alias_method :new_search, :build_search
alias_method :new_search!, :build_search!
def valid_find_options
VALID_FIND_OPTIONS
end
end
end
end