module Searchlogic module NamedScopes # Handles dynamically creating named scopes for 'OR' conditions. Please see the README for a more # detailed explanation. module OrConditions class NoConditionSpecifiedError < StandardError; end class UnknownConditionError < StandardError; end def condition?(name) # :nodoc: super || or_condition?(name) end def named_scope_options(name) # :nodoc: super || super(or_conditions(name).join("_or_")) end private def or_condition?(name) !or_conditions(name).nil? end def method_missing(name, *args, &block) if conditions = or_conditions(name) create_or_condition(conditions, args) (class << self; self; end).class_eval { alias_method name, conditions.join("_or_") } if !respond_to?(name) send(name, *args) else super end end def or_conditions(name) # First determine if we should even work on the name, we want to be as quick as possible # with this. if (parts = split_or_condition(name)).size > 1 conditions = interpolate_or_conditions(parts) if conditions.any? conditions else nil end end end def split_or_condition(name) parts = name.to_s.split("_or_") new_parts = [] parts.each do |part| if part =~ /^equal_to(_any|_all)?$/ new_parts << new_parts.pop + "_or_equal_to" else new_parts << part end end new_parts end # The purpose of this method is to convert the method name parts into actual condition names. # # Example: # # ["first_name", "last_name_like"] # => ["first_name_like", "last_name_like"] # # ["id_gt", "first_name_begins_with", "last_name", "middle_name_like"] # => ["id_gt", "first_name_begins_with", "last_name_like", "middle_name_like"] # # Basically if a column is specified without a condition the next condition in the list # is what will be used. Once we are able to get a consistent list of conditions we can easily # create a scope for it. def interpolate_or_conditions(parts) conditions = [] last_condition = nil parts.reverse.each do |part| if details = condition_details(part) # We are a searchlogic defined scope conditions << "#{details[:column]}_#{details[:condition]}" last_condition = details[:condition] elsif association_details = association_condition_details(part, last_condition) path = full_association_path(part, last_condition, association_details[:association]) conditions << "#{path[:path].join("_").to_sym}_#{path[:column]}_#{path[:condition]}" last_condition = path[:condition] || nil elsif local_condition?(part) # We are a custom scope conditions << part elsif column_names.include?(part) # we are a column, use the last condition if last_condition.nil? raise NoConditionSpecifiedError.new("The '#{part}' column doesn't know which condition to use, if you use an exact column " + "name you need to specify a condition sometime after (ex: id_or_created_at_lt), where id would use the 'lt' condition.") end conditions << "#{part}_#{last_condition}" else raise UnknownConditionError.new("The condition '#{part}' is not a valid condition, we could not find any scopes that match this.") end end conditions.reverse end def full_association_path(part, last_condition, given_assoc) path = [given_assoc.to_sym] part.sub!(/^#{given_assoc}_/, "") klass = self while klass = klass.send(:reflect_on_association, given_assoc.to_sym) klass = klass.klass if details = klass.send(:association_condition_details, part, last_condition) path << details[:association] part = details[:condition] given_assoc = details[:association] elsif details = klass.send(:condition_details, part) return { :path => path, :column => details[:column], :condition => details[:condition] } end end { :path => path, :column => part, :condition => last_condition } end def create_or_condition(scopes, args) scopes_options = scopes.collect { |scope, *args| send(scope, *args).proxy_options } # We're using first scope to determine column's type scope = named_scope_options(scopes.first) column_type = scope.respond_to?(:searchlogic_arg_type) ? scope.searchlogic_arg_type : :string named_scope scopes.join("_or_"), searchlogic_lambda(column_type) { |*args| merge_scopes_with_or(scopes.collect { |scope| send(scope, *args) }) } end def merge_scopes_with_or(scopes) scopes_options = scopes.collect { |scope| scope.proxy_options } conditions = scopes_options.reject { |o| o[:conditions].nil? }.collect { |o| sanitize_sql(o[:conditions]) } scope = scopes_options.inject(scoped({})) { |current_scope, options| current_scope.scoped(options) } options = scope.scope(:find) options.delete(:readonly) unless scope.proxy_options.key?(:readonly) options.merge(:conditions => "(" + conditions.join(") OR (") + ")") end end end end