diff --git a/lib/ransack/adapters/active_record.rb b/lib/ransack/adapters/active_record.rb index d3cf086..5b6fa92 100644 --- a/lib/ransack/adapters/active_record.rb +++ b/lib/ransack/adapters/active_record.rb @@ -1,7 +1,18 @@ -require 'ransack/adapters/active_record/base' -require 'ransack/adapters/active_record/join_dependency' -require 'ransack/adapters/active_record/join_association' -require 'ransack/adapters/active_record/context' +case ActiveRecord::VERSION::STRING +when /^3\.0\./ + require 'ransack/adapters/active_record/3.0/base' + require 'ransack/adapters/active_record/3.0/join_dependency' + require 'ransack/adapters/active_record/3.0/join_association' + require 'ransack/adapters/active_record/3.0/context' -ActiveRecord::Base.extend Ransack::Adapters::ActiveRecord::Base -ActiveRecord::Associations::JoinDependency.send :include, Ransack::Adapters::ActiveRecord::JoinDependency \ No newline at end of file + ActiveRecord::Base.extend Ransack::Adapters::ActiveRecord::Base + ActiveRecord::Associations::ClassMethods::JoinDependency.send :include, Ransack::Adapters::ActiveRecord::JoinDependency +else + require 'ransack/adapters/active_record/base' + require 'ransack/adapters/active_record/join_dependency' + require 'ransack/adapters/active_record/join_association' + require 'ransack/adapters/active_record/context' + + ActiveRecord::Base.extend Ransack::Adapters::ActiveRecord::Base + ActiveRecord::Associations::JoinDependency.send :include, Ransack::Adapters::ActiveRecord::JoinDependency +end \ No newline at end of file diff --git a/lib/ransack/adapters/active_record/3.0/base.rb b/lib/ransack/adapters/active_record/3.0/base.rb new file mode 100644 index 0000000..6480e77 --- /dev/null +++ b/lib/ransack/adapters/active_record/3.0/base.rb @@ -0,0 +1,34 @@ +module Ransack + module Adapters + module ActiveRecord + module Base + + def self.extended(base) + alias :search :ransack unless base.method_defined? :search + base.instance_eval do + class_attribute :_ransackers + self._ransackers ||= {} + end + end + + def ransack(params = {}, options = {}) + Search.new(self, params, options) + end + + def ransacker(name, opts = {}, &block) + Ransacker.new(self, name, opts, &block) + end + + # TODO: Let's actually do some authorization. Whitelist-only. + def ransackable_attributes(auth_object) + column_names + _ransackers.keys + end + + def ransackable_associations(auth_object) + reflect_on_all_associations.map {|a| a.name.to_s} + end + + end + end + end +end \ No newline at end of file diff --git a/lib/ransack/adapters/active_record/3.0/compat.rb b/lib/ransack/adapters/active_record/3.0/compat.rb new file mode 100644 index 0000000..8c2ab17 --- /dev/null +++ b/lib/ransack/adapters/active_record/3.0/compat.rb @@ -0,0 +1,23 @@ +# UGLY, UGLY MONKEY PATCHES FOR BACKWARDS COMPAT!!! AVERT YOUR EYES!! +if Arel::Nodes::And < Arel::Nodes::Binary + class Ransack::Visitor + def visit_Ransack_Nodes_And(object) + nodes = object.values.map {|o| accept(o)}.compact + return nil unless nodes.size > 0 + + if nodes.size > 1 + nodes.inject(&:and) + else + nodes.first + end + end + end +end + +class ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase + def table + Arel::Table.new(table_name, :as => aliased_table_name, + :engine => active_record.arel_engine, + :columns => active_record.columns) + end +end \ No newline at end of file diff --git a/lib/ransack/adapters/active_record/3.0/context.rb b/lib/ransack/adapters/active_record/3.0/context.rb new file mode 100644 index 0000000..3e81008 --- /dev/null +++ b/lib/ransack/adapters/active_record/3.0/context.rb @@ -0,0 +1,168 @@ +require 'ransack/context' +require 'active_record' +require 'ransack/adapters/active_record/3.0/compat' + +module Ransack + + module Adapters + module ActiveRecord + class Context < ::Ransack::Context + # Because the AR::Associations namespace is insane + JoinDependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency + JoinBase = JoinDependency::JoinBase + + def evaluate(search, opts = {}) + viz = Visitor.new + relation = @object.where(viz.accept(search.base)).order(viz.accept(search.sorts)) + opts[:distinct] ? relation.select("DISTINCT #{@klass.quoted_table_name}.*") : relation + end + + def attribute_method?(str, klass = @klass) + exists = false + + if ransackable_attribute?(str, klass) + exists = true + elsif (segments = str.split(/_/)).size > 1 + remainder = [] + found_assoc = nil + while !found_assoc && remainder.unshift(segments.pop) && segments.size > 0 do + assoc, poly_class = unpolymorphize_association(segments.join('_')) + if found_assoc = get_association(assoc, klass) + exists = attribute_method?(remainder.join('_'), poly_class || found_assoc.klass) + end + end + end + + exists + end + + def table_for(parent) + parent.table + end + + def klassify(obj) + if Class === obj && ::ActiveRecord::Base > obj + obj + elsif obj.respond_to? :klass + obj.klass + elsif obj.respond_to? :active_record + obj.active_record + else + raise ArgumentError, "Don't know how to klassify #{obj}" + end + end + + def type_for(attr) + return nil unless attr + name = attr.name.to_s + table = attr.relation.name + + unless @engine.connection.table_exists?(table) + raise "No table named #{table} exists" + end + + # TODO: optimize + @engine.connection.columns(table).detect {|c| c.name == name}.type + end + + private + + def get_parent_and_attribute_name(str, parent = @base) + attr_name = nil + + if ransackable_attribute?(str, klassify(parent)) + attr_name = str + elsif (segments = str.split(/_/)).size > 1 + remainder = [] + found_assoc = nil + while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do + assoc, klass = unpolymorphize_association(segments.join('_')) + if ransackable_association?(assoc, klassify(parent)) + found_assoc = get_association(assoc, parent) + join = build_or_find_association(found_assoc.name, parent, klass) + parent, attr_name = get_parent_and_attribute_name(remainder.join('_'), join) + end + end + end + + [parent, attr_name] + end + + def ransackable_attribute?(str, klass) + klass.ransackable_attributes(auth_object).include? str + end + + def ransackable_association?(str, klass) + klass.ransackable_associations(auth_object).include? str + end + + def get_association(str, parent = @base) + klassify(parent).reflect_on_all_associations.detect {|a| a.name.to_s == str} + end + + def join_dependency(relation) + if relation.respond_to?(:join_dependency) # Squeel will enable this + relation.join_dependency + else + build_join_dependency(relation) + end + end + + def build_join_dependency(relation) + buckets = relation.joins_values.group_by do |join| + case join + when String + 'string_join' + when Hash, Symbol, Array + 'association_join' + when ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation + 'stashed_join' + when Arel::Nodes::Join + 'join_node' + else + raise 'unknown class: %s' % join.class.name + end + end + + association_joins = buckets['association_join'] || [] + stashed_association_joins = buckets['stashed_join'] || [] + join_nodes = buckets['join_node'] || [] + string_joins = (buckets['string_join'] || []).map { |x| + x.strip + }.uniq + + join_list = relation.send :custom_join_sql, (string_joins + join_nodes) + + join_dependency = JoinDependency.new( + relation.klass, + association_joins, + join_list + ) + + join_nodes.each do |join| + join_dependency.table_aliases[join.left.name.downcase] = 1 + end + + join_dependency.graft(*stashed_association_joins) + end + + def build_or_find_association(name, parent = @base, klass = nil) + found_association = @join_dependency.join_associations.detect do |assoc| + assoc.reflection.name == name && + assoc.parent == parent && + (!klass || assoc.klass == klass) + end + unless found_association + @join_dependency.send(:build_polymorphic, name.to_sym, parent, Arel::Nodes::OuterJoin, klass) + found_association = @join_dependency.join_associations.last + # Leverage the stashed association functionality in AR + @object = @object.joins(found_association) + end + + found_association + end + + end + end + end +end \ No newline at end of file diff --git a/lib/ransack/adapters/active_record/3.0/join_association.rb b/lib/ransack/adapters/active_record/3.0/join_association.rb new file mode 100644 index 0000000..9f65184 --- /dev/null +++ b/lib/ransack/adapters/active_record/3.0/join_association.rb @@ -0,0 +1,44 @@ +require 'active_record' + +module Ransack + module Adapters + module ActiveRecord + class JoinAssociation < ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation + + def initialize(reflection, join_dependency, parent = nil, polymorphic_class = nil) + if polymorphic_class && ::ActiveRecord::Base > polymorphic_class + swapping_reflection_klass(reflection, polymorphic_class) do |reflection| + super(reflection, join_dependency, parent) + end + else + super(reflection, join_dependency, parent) + end + end + + def swapping_reflection_klass(reflection, klass) + reflection = reflection.clone + original_polymorphic = reflection.options.delete(:polymorphic) + reflection.instance_variable_set(:@klass, klass) + yield reflection + ensure + reflection.options[:polymorphic] = original_polymorphic + end + + def ==(other) + super && active_record == other.active_record + end + + def build_constraint(reflection, table, key, foreign_table, foreign_key) + if reflection.options[:polymorphic] + super.and( + foreign_table[reflection.foreign_type].eq(reflection.klass.name) + ) + else + super + end + end + + end + end + end +end \ No newline at end of file diff --git a/lib/ransack/adapters/active_record/3.0/join_dependency.rb b/lib/ransack/adapters/active_record/3.0/join_dependency.rb new file mode 100644 index 0000000..3d2634a --- /dev/null +++ b/lib/ransack/adapters/active_record/3.0/join_dependency.rb @@ -0,0 +1,63 @@ +require 'active_record' + +module Ransack + module Adapters + module ActiveRecord + module JoinDependency + + # Yes, I'm using alias_method_chain here. No, I don't feel too + # bad about it. JoinDependency, or, to call it by its full proper + # name, ::ActiveRecord::Associations::JoinDependency, is one of the + # most "for internal use only" chunks of ActiveRecord. + def self.included(base) + base.class_eval do + alias_method_chain :graft, :ransack + end + end + + def graft_with_ransack(*associations) + associations.each do |association| + join_associations.detect {|a| association == a} || + build_polymorphic(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type, association.reflection.klass) + end + self + end + + # Should only be called by Ransack, and only with a single association name + def build_polymorphic(association, parent = nil, join_type = Arel::OuterJoin, klass = nil) + parent ||= joins.last + reflection = parent.reflections[association] or + raise ::ActiveRecord::ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" + unless join_association = find_join_association_respecting_polymorphism(reflection, parent, klass) + @reflections << reflection + join_association = build_join_association_respecting_polymorphism(reflection, parent, klass) + join_association.join_type = join_type + @joins << join_association + cache_joined_association(join_association) + end + + join_association + end + + def find_join_association_respecting_polymorphism(reflection, parent, klass) + if association = find_join_association(reflection, parent) + unless reflection.options[:polymorphic] + association + else + association if association.active_record == klass + end + end + end + + def build_join_association_respecting_polymorphism(reflection, parent, klass = nil) + if reflection.options[:polymorphic] && klass + JoinAssociation.new(reflection, self, parent, klass) + else + JoinAssociation.new(reflection, self, parent) + end + end + + end + end + end +end \ No newline at end of file diff --git a/lib/ransack/version.rb b/lib/ransack/version.rb index 523f140..825723a 100644 --- a/lib/ransack/version.rb +++ b/lib/ransack/version.rb @@ -1,3 +1,3 @@ module Ransack - VERSION = "0.1.0" + VERSION = "0.2.0" end diff --git a/ransack.gemspec b/ransack.gemspec index a78c3f0..8893e8c 100644 --- a/ransack.gemspec +++ b/ransack.gemspec @@ -14,9 +14,9 @@ Gem::Specification.new do |s| s.rubyforge_project = "ransack" - s.add_dependency 'activerecord', '~> 3.1.0.alpha' - s.add_dependency 'activesupport', '~> 3.1.0.alpha' - s.add_dependency 'actionpack', '~> 3.1.0.alpha' + s.add_dependency 'activerecord', '~> 3.0' + s.add_dependency 'activesupport', '~> 3.0' + s.add_dependency 'actionpack', '~> 3.0' s.add_development_dependency 'rspec', '~> 2.5.0' s.add_development_dependency 'machinist', '~> 1.0.6' s.add_development_dependency 'faker', '~> 0.9.5' diff --git a/spec/console.rb b/spec/console.rb index 24677d8..f44c059 100644 --- a/spec/console.rb +++ b/spec/console.rb @@ -2,6 +2,7 @@ Bundler.setup require 'machinist/active_record' require 'sham' require 'faker' +require 'ransack' Dir[File.expand_path('../../spec/{helpers,support,blueprints}/*.rb', __FILE__)].each do |f| require f @@ -18,5 +19,3 @@ end Schema.create -require 'ransack' - diff --git a/spec/ransack/adapters/active_record/base_spec.rb b/spec/ransack/adapters/active_record/base_spec.rb index 162c3af..72e86f0 100644 --- a/spec/ransack/adapters/active_record/base_spec.rb +++ b/spec/ransack/adapters/active_record/base_spec.rb @@ -39,7 +39,7 @@ module Ransack it 'allows an "attribute" to be an InfixOperation' do s = Person.search(:doubled_name_eq => 'Aric SmithAric Smith') s.result.first.should eq Person.find_by_name('Aric Smith') - end + end if defined?(Arel::Nodes::InfixOperation) end end diff --git a/spec/ransack/adapters/active_record/context_spec.rb b/spec/ransack/adapters/active_record/context_spec.rb index 17943d4..e3fe3f9 100644 --- a/spec/ransack/adapters/active_record/context_spec.rb +++ b/spec/ransack/adapters/active_record/context_spec.rb @@ -11,14 +11,14 @@ module Ransack it 'contextualizes strings to attributes' do attribute = @c.contextualize 'children_children_parent_name' attribute.should be_a Arel::Attributes::Attribute - attribute.name.should eq 'name' + attribute.name.to_s.should eq 'name' attribute.relation.table_alias.should eq 'parents_people' end it 'builds new associations if not yet built' do attribute = @c.contextualize 'children_articles_title' attribute.should be_a Arel::Attributes::Attribute - attribute.name.should eq 'title' + attribute.name.to_s.should eq 'title' attribute.relation.name.should eq 'articles' attribute.relation.table_alias.should be_nil end diff --git a/spec/ransack/helpers/form_builder_spec.rb b/spec/ransack/helpers/form_builder_spec.rb index e6bff49..eef12a2 100644 --- a/spec/ransack/helpers/form_builder_spec.rb +++ b/spec/ransack/helpers/form_builder_spec.rb @@ -20,6 +20,10 @@ module Ransack include router.url_helpers end + @controller.view_context_class.class_eval do + include router.url_helpers + end + @s = Person.search @controller.view_context.search_form_for @s do |f| @f = f diff --git a/spec/ransack/search_spec.rb b/spec/ransack/search_spec.rb index 8130140..bb0944a 100644 --- a/spec/ransack/search_spec.rb +++ b/spec/ransack/search_spec.rb @@ -109,7 +109,7 @@ module Ransack ) search.result.should be_an ActiveRecord::Relation where = search.result.where_values.first - where.to_sql.should match /\("children_people"."name" = 'Ernie' AND \("people"."name" = 'Ernie' OR "children_people_2"."name" = 'Ernie'\)\)/ + where.to_sql.should match /"children_people"."name" = 'Ernie' AND \("people"."name" = 'Ernie' OR "children_people_2"."name" = 'Ernie'\)/ end it 'evaluates arrays of groupings' do @@ -121,7 +121,7 @@ module Ransack ) search.result.should be_an ActiveRecord::Relation where = search.result.where_values.first - where.to_sql.should match /\(\("people"."name" = 'Ernie' OR "children_people"."name" = 'Ernie'\) AND \("people"."name" = 'Bert' OR "children_people"."name" = 'Bert'\)\)/ + where.to_sql.should match /\("people"."name" = 'Ernie' OR "children_people"."name" = 'Ernie'\) AND \("people"."name" = 'Bert' OR "children_people"."name" = 'Bert'\)/ end it 'returns distinct records when passed :distinct => true' do