module Searchlogic module NamedScopes # Handles dynamically creating named scopes for columns. module Conditions COMPARISON_CONDITIONS = { :equals => [:is, :eq], :does_not_equal => [:not_equal_to, :is_not, :not, :ne], :less_than => [:lt, :before], :less_than_or_equal_to => [:lte], :greater_than => [:gt, :after], :greater_than_or_equal_to => [:gte], } WILDCARD_CONDITIONS = { :like => [:contains, :includes], :not_like => [], :begins_with => [:bw], :not_begin_with => [:does_not_begin_with], :ends_with => [:ew], :not_end_with => [:does_not_end_with] } BOOLEAN_CONDITIONS = { :null => [:nil], :not_null => [:not_nil], :empty => [] } CONDITIONS = {} COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases| CONDITIONS[condition] = aliases CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym } CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym } end BOOLEAN_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases } PRIMARY_CONDITIONS = CONDITIONS.keys ALIAS_CONDITIONS = CONDITIONS.values.flatten # Retrieves the options passed when creating the respective named scope. Ex: # # named_scope :whatever, :conditions => {:column => value} # # This method will return: # # :conditions => {:column => value} # # ActiveRecord hides this internally, so we have to try and pull it out with this # method. def named_scope_options(name) key = scopes.key?(name.to_sym) ? name.to_sym : primary_condition_name(name) if key eval("options", scopes[key]) else nil end end # The arity for a named scope's proc is important, because we use the arity # to determine if the condition should be ignored when calling the search method. # If the condition is false and the arity is 0, then we skip it all together. Ex: # # User.named_scope :age_is_4, :conditions => {:age => 4} # User.search(:age_is_4 => false) == User.all # User.search(:age_is_4 => true) == User.all(:conditions => {:age => 4}) # # We also use it when trying to "copy" the underlying named scope for association # conditions. def named_scope_arity(name) options = named_scope_options(name) options.respond_to?(:arity) ? options.arity : nil end # Returns the primary condition for the given alias. Ex: # # primary_condition(:gt) => :greater_than def primary_condition(alias_condition) CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first end # Returns the primary name for any condition on a column. You can pass it # a primary condition, alias condition, etc, and it will return the proper # primary condition name. This helps simply logic throughout Searchlogic. Ex: # # primary_condition_name(:id_gt) => :id_greater_than # primary_condition_name(:id_greater_than) => :id_greater_than def primary_condition_name(name) if primary_condition?(name) name.to_sym elsif details = alias_condition_details(name) "#{details[:column]}_#{primary_condition(details[:condition])}".to_sym else nil end end # Is the name of the method a valid condition that can be dynamically created? def condition?(name) local_condition?(name) end # Is the condition for a local column, not an association def local_condition?(name) primary_condition?(name) || alias_condition?(name) end # Is the name of the method a valid condition that can be dynamically created, # AND is it a primary condition (not an alias). "greater_than" not "gt". def primary_condition?(name) !primary_condition_details(name).nil? end # Is the name of the method a valid condition that can be dynamically created, # AND is it an alias condition. "gt" not "greater_than". def alias_condition?(name) !alias_condition_details(name).nil? end private def method_missing(name, *args, &block) if details = primary_condition_details(name) create_primary_condition(details[:column], details[:condition]) send(name, *args) elsif details = alias_condition_details(name) create_alias_condition(details[:column], details[:condition], args) send(name, *args) else super end end def primary_condition_details(name) if name.to_s =~ /^(#{column_names.join("|")})_(#{PRIMARY_CONDITIONS.join("|")})$/ {:column => $1, :condition => $2} end end def create_primary_condition(column, condition) column_type = columns_hash[column.to_s].type match_keyword = ActiveRecord::Base.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE" scope_options = case condition.to_s when /^equals/ scope_options(condition, column_type, "#{table_name}.#{column} = ?") when /^does_not_equal/ scope_options(condition, column_type, "#{table_name}.#{column} != ?") when /^less_than_or_equal_to/ scope_options(condition, column_type, "#{table_name}.#{column} <= ?") when /^less_than/ scope_options(condition, column_type, "#{table_name}.#{column} < ?") when /^greater_than_or_equal_to/ scope_options(condition, column_type, "#{table_name}.#{column} >= ?") when /^greater_than/ scope_options(condition, column_type, "#{table_name}.#{column} > ?") when /^like/ scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :like) when /^not_like/ scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :like) when /^begins_with/ scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :begins_with) when /^not_begin_with/ scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :begins_with) when /^ends_with/ scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :ends_with) when /^not_end_with/ scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :ends_with) when "null" {:conditions => "#{table_name}.#{column} IS NULL"} when "not_null" {:conditions => "#{table_name}.#{column} IS NOT NULL"} when "empty" {:conditions => "#{table_name}.#{column} = ''"} end named_scope("#{column}_#{condition}".to_sym, scope_options) end # This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions. # Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as # fast as possible, which is why I didn't do the case statement inside of the lambda. def scope_options(condition, column_type, sql, value_modifier = nil) case condition.to_s when /_(any|all)$/ searchlogic_lambda(column_type) { |*values| return {} if values.empty? values = values.flatten values_to_sub = nil if value_modifier.nil? values_to_sub = values else values_to_sub = values.collect { |value| value_with_modifier(value, value_modifier) } end join = $1 == "any" ? " OR " : " AND " {:conditions => [values.collect { |value| sql }.join(join), *values_to_sub]} } else searchlogic_lambda(column_type) { |value| {:conditions => [sql, value_with_modifier(value, value_modifier)]} } end end def value_with_modifier(value, modifier) case modifier when :like "%#{value}%" when :begins_with "#{value}%" when :ends_with "%#{value}" else value end end def alias_condition_details(name) if name.to_s =~ /^(#{column_names.join("|")})_(#{ALIAS_CONDITIONS.join("|")})$/ {:column => $1, :condition => $2} end end def create_alias_condition(column, condition, args) primary_condition = primary_condition(condition) alias_name = "#{column}_#{condition}" primary_name = "#{column}_#{primary_condition}" send(primary_name, *args) # go back to method_missing and make sure we create the method (class << self; self; end).class_eval { alias_method alias_name, primary_name } end end end end