3.0 backporting work in progress
This commit is contained in:
parent
7264896d43
commit
742c638af6
|
@ -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
|
||||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,3 +1,3 @@
|
|||
module Ransack
|
||||
VERSION = "0.1.0"
|
||||
VERSION = "0.2.0"
|
||||
end
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue