Skip to content

Commit

Permalink
working on association conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffp committed Jun 30, 2010
1 parent 9c366b8 commit 1b63d0c
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 255 deletions.
7 changes: 6 additions & 1 deletion Rakefile
Expand Up @@ -21,7 +21,12 @@ end
require 'spec/rake/spectask'
Spec::Rake::SpecTask.new(:spec) do |spec|
spec.libs << 'lib' << 'spec'
spec.spec_files = FileList['spec/**/*_spec.rb']
spec.spec_files = FileList[
#'spec/**/conditions_spec.rb',
#'spec/**/ordering_conditions_spec.rb',
'spec/**/association_conditions_spec.rb'
#'spec/**/*_spec.rb'
]
end

Spec::Rake::SpecTask.new(:rcov) do |spec|
Expand Down
6 changes: 4 additions & 2 deletions lib/searchlogic.rb
Expand Up @@ -11,6 +11,7 @@
require "searchlogic/named_scopes/or_conditions"
require "searchlogic/search"

ActiveRecord::Relation.send(:include, Searchlogic::CoreExt::Proc)
Proc.send(:include, Searchlogic::CoreExt::Proc)
Object.send(:include, Searchlogic::CoreExt::Object)

Expand All @@ -28,9 +29,10 @@ class AssociationProxy
end

m = Searchlogic::NamedScopes
[Searchlogic::ActiveRecord::NamedScopeTools,
[
Searchlogic::ActiveRecord::NamedScopeTools,
m::Conditions,
# m::AssociationConditions,
m::AssociationConditions,
# m::AssociationOrdering,
m::Ordering,
# m::AliasScope
Expand Down
1 change: 1 addition & 0 deletions lib/searchlogic/active_record/named_scope_tools.rb
Expand Up @@ -16,6 +16,7 @@ def named_scope_options(name)
key = scopes.key?(name.to_sym) ? name.to_sym : condition_scope_name(name)

if key
#TODO: recover the find options from relation
eval("options", scopes[key].binding)
else
nil
Expand Down
2 changes: 1 addition & 1 deletion lib/searchlogic/core_ext/proc.rb
Expand Up @@ -14,4 +14,4 @@ def searchlogic_options
end
end
end
end
end
174 changes: 101 additions & 73 deletions lib/searchlogic/named_scopes/association_conditions.rb
Expand Up @@ -5,6 +5,22 @@ module AssociationConditions
def condition?(name) # :nodoc:
super || association_condition?(name)
end

def _resolve_deep_association_conditions(condition_name, args)
if local_condition?(condition_name)
[self.table_name.to_sym, self.send(condition_name, *args)]
elsif details = association_condition_details(condition_name)
resolution = details[:association].klass._resolve_deep_association_conditions(details[:condition], args)
return nil unless resolution #method did not resolve
this_table = self.table_name.to_sym
join_list = resolution.first.nil? ? this_table : {this_table=>resolution.first}
[join_list, resolution.last]
else #this method did not resolve
nil
end
end



private
def association_condition?(name)
Expand All @@ -13,13 +29,25 @@ def association_condition?(name)

def method_missing(name, *args, &block)
if !local_condition?(name) && details = association_condition_details(name)
create_association_condition(details[:association], details[:condition], args, details[:poly_class])
create_scope_for_association(details[:association], details[:condition], args, details[:poly_class])
send(name, *args)
else
super
end
end


def create_scope_for_association(association, condition_name, args, poly_class = nil)
resolution = association.klass._resolve_deep_association_conditions(condition_name, args)
return unless resolution

final_condition = resolution.last
join_hierarchy = resolution.first
algebra = final_condition.joins(join_hierarchy)

name = [association.name, poly_class && "#{poly_class.name.underscore}_type", condition_name].compact.join("_")
scope(name, algebra)
end

def association_condition_details(name, last_condition = nil)
non_poly_assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
poly_assocs = reflect_on_all_associations.reject { |assoc| !assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
Expand Down Expand Up @@ -51,81 +79,81 @@ def association_condition_details(name, last_condition = nil)
end
end

def create_association_condition(association, condition_name, args, poly_class = nil)
name = [association.name, poly_class && "#{poly_class.name.underscore}_type", condition_name].compact.join("_")
scope(name, association_condition_options(association, condition_name, args, poly_class))
end

def association_condition_options(association, association_condition, args, poly_class = nil)
klass = poly_class ? poly_class : association.klass
scope = klass.send(association_condition, *args)
scope_options = klass.named_scope_options(association_condition)
arity = klass.named_scope_arity(association_condition)

if !arity || arity == 0
# The underlying condition doesn't require any parameters, so let's just create a simple
# named scope that is based on a hash.
options = {}
in_searchlogic_delegation { options = scope.scope(:find) }
prepare_named_scope_options(options, association, poly_class)
options
else
proc_args = arity_args(arity)
arg_type = (scope_options.respond_to?(:searchlogic_options) && scope_options.searchlogic_options[:type]) || :string

eval <<-"end_eval"
searchlogic_lambda(:#{arg_type}) { |#{proc_args.join(",")}|
options = {}
in_searchlogic_delegation do
scope = klass.send(association_condition, #{proc_args.join(",")})
options = scope.scope(:find) if scope
end
prepare_named_scope_options(options, association, poly_class)
options
}
end_eval
end
end
# def association_condition_options(association, association_condition, args, poly_class = nil)
# klass = poly_class ? poly_class : association.klass
# relation = klass.send(association_condition, *args)
# scope_options = nil #klass.named_scope_options(association_condition)
# arity = -1 #klass.named_scope_arity(association_condition)
#
# if !arity || arity == 0
# # The underlying condition doesn't require any parameters, so let's just create a simple
# # named scope that is based on a hash.
# options = {}
# in_searchlogic_delegation { options = relation.scope(:find) }
# prepare_named_scope_options(options, association, poly_class)
# options
# else
# proc_args = arity_args(arity)
# arg_type = :string #(scope_options.respond_to?(:searchlogic_options) && scope_options.searchlogic_options[:type]) || :string
#
# eval <<-"end_eval"
# searchlogic_lambda(:#{arg_type}) { |#{proc_args.join(",")}|
# options = {}
#
# in_searchlogic_delegation do
# relation = klass.send(association_condition, #{proc_args.join(",")})
# options = {:conditions=>"users.username LIKE '%joe%'"} #relation.scope(:find) if relation
# end
#
# prepare_named_scope_options(options, association, poly_class)
# options
# }
# end_eval
# end
# end

# Used to match the new scopes parameters to the underlying scope. This way we can disguise the
# new scope as best as possible instead of taking the easy way out and using *args.
def arity_args(arity)
args = []
if arity > 0
arity.times { |i| args << "arg#{i}" }
else
positive_arity = arity * -1
positive_arity.times do |i|
if i == (positive_arity - 1)
args << "*arg#{i}"
else
args << "arg#{i}"
end
end
end
args
end

def prepare_named_scope_options(options, association, poly_class = nil)
options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that

klass = poly_class || association.klass
# sanitize the conditions locally so we get the right table name, otherwise the conditions will be evaluated on the original model
options[:conditions] = klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)

poly_join = poly_class && inner_polymorphic_join(poly_class.name.underscore, :as => association.name)

if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
options[:joins] = [poly_class ? poly_join : inner_joins(association.name), options[:joins]].flatten
elsif poly_class
options[:joins] = options[:joins].blank? ? poly_join : ([poly_join] + klass.inner_joins(options[:joins]))
else
options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
end
end
# def arity_args(arity)
# args = []
# if arity > 0
# arity.times { |i| args << "arg#{i}" }
# else
# positive_arity = arity * -1
# positive_arity.times do |i|
# if i == (positive_arity - 1)
# args << "*arg#{i}"
# else
# args << "arg#{i}"
# end
# end
# end
# args
# end

# #ADDED: this was removed from AR::Base ver2.x, redefined for use in prepare_named_scope_options
# def array_of_strings?(o)
# o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
# end
#
# def prepare_named_scope_options(options, association, poly_class = nil)
# options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
#
# klass = poly_class || association.klass
# # sanitize the conditions locally so we get the right table name, otherwise the conditions will be evaluated on the original model
# options[:conditions] = klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)
#
# poly_join = poly_class && inner_polymorphic_join(poly_class.name.underscore, :as => association.name)
#
# if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
# options[:joins] = [poly_class ? poly_join : inner_joins(association.name), options[:joins]].flatten
# elsif poly_class
# options[:joins] = options[:joins].blank? ? poly_join : ([poly_join] + klass.inner_joins(options[:joins]))
# else
# options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
# end
# end
end
end
end
13 changes: 7 additions & 6 deletions lib/searchlogic/named_scopes/conditions.rb
Expand Up @@ -71,7 +71,7 @@ def boolean_condition?(name)

def method_missing(name, *args, &block)
if details = condition_details(name)
create_condition(details[:column], details[:condition], args)
create_scope(details[:column], details[:condition], args)
send(name, *args)
elsif boolean_condition?(name)
column = name.to_s.gsub(/^not_/, "")
Expand All @@ -84,6 +84,7 @@ def method_missing(name, *args, &block)


def condition_details(method_name)
#TODO: cache these
column_name_matcher = column_names.join("|")
conditions_matcher = (PRIMARY_CONDITIONS + ALIAS_CONDITIONS).join("|")

Expand All @@ -92,15 +93,15 @@ def condition_details(method_name)
end
end

def create_condition(column, condition, args)
def create_scope(column, condition, args)
if PRIMARY_CONDITIONS.include?(condition.to_sym)
create_primary_condition(column, condition)
create_primary_scope(column, condition)
elsif ALIAS_CONDITIONS.include?(condition.to_sym)
create_alias_condition(column, condition, args)
create_aliased_scope(column, condition, args)
end
end

def create_primary_condition(column, condition)
def create_primary_scope(column, condition)
column_type = columns_hash[column.to_s].type
skip_conversion = skip_time_zone_conversion_for_attributes.include?(columns_hash[column.to_s].name.to_sym)
match_keyword = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE"
Expand Down Expand Up @@ -190,7 +191,7 @@ def value_with_modifier(value, modifier)
end
end

def create_alias_condition(column, condition, args)
def create_aliased_scope(column, condition, args)
primary_condition = primary_condition(condition)
alias_name = "#{column}_#{condition}"
primary_name = "#{column}_#{primary_condition}"
Expand Down

0 comments on commit 1b63d0c

Please sign in to comment.