diff --git a/lib/ransack.rb b/lib/ransack.rb index 262cf35..b2a016d 100644 --- a/lib/ransack.rb +++ b/lib/ransack.rb @@ -20,9 +20,11 @@ end require 'ransack/translate' require 'ransack/adapters/active_record/ransack/translate' if defined?(::ActiveRecord::Base) +require 'ransack/adapters/mongoid/ransack/translate' if defined?(::Mongoid) require 'ransack/search' require 'ransack/ransacker' require 'ransack/adapters/active_record' if defined?(::ActiveRecord::Base) +require 'ransack/adapters/mongoid' if defined?(::Mongoid) require 'ransack/helpers' require 'action_controller' diff --git a/lib/ransack/adapters/mongoid.rb b/lib/ransack/adapters/mongoid.rb new file mode 100644 index 0000000..3374ca5 --- /dev/null +++ b/lib/ransack/adapters/mongoid.rb @@ -0,0 +1,9 @@ +require 'ransack/adapters/mongoid/base' +Mongoid::Document.extend Ransack::Adapters::Mongoid::Base + +case Mongoid::VERSION::STRING +when /^3\.2\./ + require 'ransack/adapters/mongoid/3.2/context' +else + require 'ransack/adapters/mongoid/context' +end diff --git a/lib/ransack/adapters/mongoid/3.2/.gitkeep b/lib/ransack/adapters/mongoid/3.2/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/ransack/adapters/mongoid/base.rb b/lib/ransack/adapters/mongoid/base.rb new file mode 100644 index 0000000..e68f114 --- /dev/null +++ b/lib/ransack/adapters/mongoid/base.rb @@ -0,0 +1,47 @@ +module Ransack + module Adapters + module Mongoid + module Base + + def self.extended(base) + alias :search :ransack unless base.respond_to? :search + base.class_eval do + class_attribute :_ransackers + self._ransackers ||= {} + end + end + + def ransack(params = {}, options = {}) + params = params.presence || {} + Search.new(self, params ? params.delete_if { + |k, v| v.blank? && v != false } : params, options) + end + + def ransacker(name, opts = {}, &block) + self._ransackers = _ransackers.merge name.to_s => Ransacker + .new(self, name, opts, &block) + end + + def ransackable_attributes(auth_object = nil) + column_names + _ransackers.keys + end + + def ransortable_attributes(auth_object = nil) + # Here so users can overwrite the attributes + # that show up in the sort_select + ransackable_attributes(auth_object) + end + + def ransackable_associations(auth_object = nil) + reflect_on_all_associations.map { |a| a.name.to_s } + end + + # For overriding with a whitelist of symbols + def ransackable_scopes(auth_object = nil) + [] + end + + end + end + end +end diff --git a/lib/ransack/adapters/mongoid/context.rb b/lib/ransack/adapters/mongoid/context.rb new file mode 100644 index 0000000..c0c1a37 --- /dev/null +++ b/lib/ransack/adapters/mongoid/context.rb @@ -0,0 +1,231 @@ +require 'ransack/context' +require 'polyamorous' + +module Ransack + module Adapters + module Mongoid + class Context < ::Ransack::Context + + # Because the AR::Associations namespace is insane + JoinDependency = ::Mongoid::Associations::JoinDependency + JoinPart = JoinDependency::JoinPart + + def initialize(object, options = {}) + super + @arel_visitor = @engine.connection.visitor + end + + def relation_for(object) + object.all + end + + def type_for(attr) + return nil unless attr && attr.valid? + name = attr.arel_attribute.name.to_s + table = attr.arel_attribute.relation.table_name + + schema_cache = @engine.connection.schema_cache + raise "No table named #{table} exists" unless schema_cache.table_exists?(table) + schema_cache.columns_hash(table)[name].type + end + + def evaluate(search, opts = {}) + viz = Visitor.new + relation = @object.where(viz.accept(search.base)) + if search.sorts.any? + relation = relation.except(:order) + .reorder(viz.accept(search.sorts)) + end + opts[:distinct] ? relation.distinct : 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 && ::Mongoid::Base > obj + obj + elsif obj.respond_to? :klass + obj.klass + elsif obj.respond_to? :base_klass + obj.base_klass + else + raise ArgumentError, "Don't know how to klassify #{obj}" + end + 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 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 get_association(str, parent = @base) + klass = klassify parent + ransackable_association?(str, klass) && + klass.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 + + # Checkout mongoid/relation/query_methods.rb +build_joins+ for + # reference. Lots of duplicated code maybe we can avoid it + 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 JoinDependency, 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_ast, + relation.table.from(relation.table), string_joins + + join_dependency = JoinDependency.new( + relation.klass, association_joins, join_list + ) + + join_nodes.each do |join| + join_dependency.alias_tracker.aliases[join.left.name.downcase] = 1 + end + + if ::Mongoid::VERSION::STRING >= '4.1' + join_dependency + else + join_dependency.graft(*stashed_association_joins) + end + end + + if ::Mongoid::VERSION::STRING >= '4.1' + + def build_or_find_association(name, parent = @base, klass = nil) + found_association = @join_dependency.join_root.children + .detect do |assoc| + assoc.reflection.name == name && + (@associations_pot.nil? || @associations_pot[assoc] == parent) && + (!klass || assoc.reflection.klass == klass) + end + + unless found_association + jd = JoinDependency.new( + parent.base_klass, + Polyamorous::Join.new(name, @join_type, klass), + [] + ) + found_association = jd.join_root.children.last + associations found_association, parent + + # TODO maybe we dont need to push associations here, we could loop + # through the @associations_pot instead + @join_dependency.join_root.children.push found_association + + # Builds the arel nodes properly for this association + @join_dependency.send( + :construct_tables!, jd.join_root, found_association + ) + + # Leverage the stashed association functionality in AR + @object = @object.joins(jd) + end + found_association + end + + def associations(assoc, parent) + @associations_pot ||= {} + @associations_pot[assoc] = parent + end + + else + + 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.reflection.klass == klass) + end + unless found_association + @join_dependency.send( + :build, + Polyamorous::Join.new(name, @join_type, klass), + parent + ) + 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 +end diff --git a/lib/ransack/adapters/mongoid/ransack/context.rb b/lib/ransack/adapters/mongoid/ransack/context.rb new file mode 100644 index 0000000..1217b3f --- /dev/null +++ b/lib/ransack/adapters/mongoid/ransack/context.rb @@ -0,0 +1,65 @@ +require 'ransack/visitor' + +module Ransack + class Context + attr_reader :arel_visitor + + class << self + + def for_class(klass, options = {}) + if klass < ActiveRecord::Base + Adapters::ActiveRecord::Context.new(klass, options) + end + end + + def for_object(object, options = {}) + case object + when ActiveRecord::Relation + Adapters::ActiveRecord::Context.new(object.klass, options) + end + end + + end # << self + + def initialize(object, options = {}) + @object = relation_for(object) + @klass = @object.klass + @join_dependency = join_dependency(@object) + @join_type = options[:join_type] || Arel::OuterJoin + @search_key = options[:search_key] || Ransack.options[:search_key] + + if ::ActiveRecord::VERSION::STRING >= "4.1" + @base = @join_dependency.join_root + @engine = @base.base_klass.arel_engine + else + @base = @join_dependency.join_base + @engine = @base.arel_engine + end + + @default_table = Arel::Table.new( + @base.table_name, :as => @base.aliased_table_name, :engine => @engine + ) + @bind_pairs = Hash.new do |hash, key| + parent, attr_name = get_parent_and_attribute_name(key.to_s) + if parent && attr_name + hash[key] = [parent, attr_name] + end + end + end + + def klassify(obj) + if Class === obj && ::ActiveRecord::Base > obj + obj + elsif obj.respond_to? :klass + obj.klass + elsif obj.respond_to? :active_record # Rails 3 + obj.active_record + elsif obj.respond_to? :base_klass # Rails 4 + obj.base_klass + else + raise ArgumentError, "Don't know how to klassify #{obj.inspect}" + end + end + + end +end diff --git a/lib/ransack/adapters/mongoid/ransack/nodes/condition.rb b/lib/ransack/adapters/mongoid/ransack/nodes/condition.rb new file mode 100644 index 0000000..eb74a37 --- /dev/null +++ b/lib/ransack/adapters/mongoid/ransack/nodes/condition.rb @@ -0,0 +1,27 @@ +module Ransack + module Nodes + class Condition + + def arel_predicate + predicates = attributes.map do |attr| + attr.attr.send( + arel_predicate_for_attribute(attr), + formatted_values_for_attribute(attr) + ) + end + + if predicates.size > 1 + case combinator + when 'and' + Arel::Nodes::Grouping.new(Arel::Nodes::And.new(predicates)) + when 'or' + predicates.inject(&:or) + end + else + predicates.first + end + end + + end # Condition + end +end diff --git a/lib/ransack/adapters/mongoid/ransack/translate.rb b/lib/ransack/adapters/mongoid/ransack/translate.rb new file mode 100644 index 0000000..a6c2730 --- /dev/null +++ b/lib/ransack/adapters/mongoid/ransack/translate.rb @@ -0,0 +1,12 @@ +module Ransack + module Translate + + def self.i18n_key(klass) + if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0 + klass.model_name.i18n_key.to_s.tr('.', '/') + else + klass.model_name.i18n_key.to_s + end + end + end +end diff --git a/lib/ransack/adapters/mongoid/ransack/visitor.rb b/lib/ransack/adapters/mongoid/ransack/visitor.rb new file mode 100644 index 0000000..1f26f86 --- /dev/null +++ b/lib/ransack/adapters/mongoid/ransack/visitor.rb @@ -0,0 +1,24 @@ +module Ransack + class Visitor + def visit_and(object) + nodes = object.values.map { |o| accept(o) }.compact + return nil unless nodes.size > 0 + + if nodes.size > 1 + Arel::Nodes::Grouping.new(Arel::Nodes::And.new(nodes)) + else + nodes.first + end + end + + def quoted?(object) + case object + when Arel::Nodes::SqlLiteral, Bignum, Fixnum + false + else + true + end + end + + end +end diff --git a/lib/ransack/context.rb b/lib/ransack/context.rb index b7747ec..a61fe0b 100644 --- a/lib/ransack/context.rb +++ b/lib/ransack/context.rb @@ -1,5 +1,6 @@ require 'ransack/visitor' require 'ransack/adapters/active_record/ransack/visitor' if defined?(::ActiveRecord::Base) +require 'ransack/adapters/mongoid/ransack/visitor' if defined?(::Mongoid) module Ransack class Context diff --git a/lib/ransack/nodes.rb b/lib/ransack/nodes.rb index acd6229..a8447cd 100644 --- a/lib/ransack/nodes.rb +++ b/lib/ransack/nodes.rb @@ -4,5 +4,6 @@ require 'ransack/nodes/attribute' require 'ransack/nodes/value' require 'ransack/nodes/condition' require 'ransack/adapters/active_record/ransack/nodes/condition' if defined?(::ActiveRecord::Base) +require 'ransack/adapters/mongoid/ransack/nodes/condition' if defined?(::Mongoid) require 'ransack/nodes/sort' require 'ransack/nodes/grouping' \ No newline at end of file diff --git a/lib/ransack/search.rb b/lib/ransack/search.rb index 11139bd..9038ebf 100644 --- a/lib/ransack/search.rb +++ b/lib/ransack/search.rb @@ -1,6 +1,7 @@ require 'ransack/nodes' require 'ransack/context' require 'ransack/adapters/active_record/ransack/context' if defined?(::ActiveRecord::Base) +require 'ransack/adapters/mongoid/ransack/context' if defined?(::Mongoid) require 'ransack/naming' module Ransack