locomotivecms/engine

View on GitHub
lib/locomotive/mongoid/patches.rb

Summary

Maintainability
A
45 mins
Test Coverage
# # encoding: utf-8

require 'mongoid'

class RawArray < ::Array
  def resizable?; false; end
end

module BSON
  class ObjectId
    def to_json(*)
      to_s.to_json
    end
    def as_json(*)
      to_s.as_json
    end
  end
end

module Mongoid #:nodoc:

  module Fields
    module ClassMethods
      alias_method :add_field_without_locomotive_patch, :add_field

      def add_field(name, options = {})
        # FIXME: The Rails ActionView inputs get the value of a field from the <FIELD>_before_type_cast method if it exists.
        # In Mongoid 7, the Mongoid core team implemented the `_before_type_cast?` / `_before_type_cast` 
        # for ALL the fields including the localized ones.
        # Incidentally, it breaks the form inputs for localized fields. 
        # This patch restores the old behavior for localized fiels by implementing the X_came_from_user? method
        # which makes the form inputs use the translated value of a field.
        generated_methods.module_eval do
          re_define_method("#{name}_came_from_user?") do
            false
          end
        end if options[:localize]

        add_field_without_locomotive_patch(name, options)
      end
    end
  end

  # FIXME: the Origin (used by Steam) and Mongoid gems both modify the Symbol class
  # to allow writing queries like .where(:position.lt => 1)
  # By convention, Origin::Key will be the one. So we need to make sure it doesn't
  # break the Mongoid queries.
  class Criteria
    module Queryable
      module Selectable
        def selection(criterion = nil)
          clone.tap do |query|
            if criterion
              criterion.each_pair do |field, value|
                _field = field.is_a?(Key) || field.is_a?(Origin::Key) ? field : field.to_s
                yield(query.selector, _field, value)
              end
            end
            query.reset_strategies!
          end
        end
      end
    end
  end

  # Hack to give a specific type to the EditableElement
  module Association
    module Embedded
      class EmbedsMany
        class Proxy < Association::Many
          def with_same_class!(document, klass)
            return document if document.is_a?(klass)

            Factory.build(klass, {}).tap do |_document|
              # make it part of the relationship
              _document.apply_post_processed_defaults
              integrate(_document)

              # make it look like the old document
              attributes = document.attributes
              attributes['_type'] = _document._type

              _document.instance_variable_set(:@attributes, attributes)
              _document.new_record  = document.new_record?
              _document._index      = document._index

              # finally, make the change in the lists
              _target[_document._index]   = _document
              _unscoped[_document._index] = _document
            end
          end
        end
      end
    end
  end

  module Document #:nodoc:
    def as_json(options = {})
      attrs = super(options)
      attrs['id'] = attrs['_id']
      attrs
    end
  end

  class Criteria
    def first!
      self.first.tap do |model|
        if model.nil?
          raise Mongoid::Errors::DocumentNotFound.new(self.klass, self.selector)
        end
      end
    end

    def without_sorting
      clone.tap { |crit| crit.options.delete(:sort) }
    end

    # http://code.dblock.org/paging-and-iterating-over-large-mongo-collections
    def each_by(by, &block)
      idx = total = 0
      set_limit = options[:limit]
      while ((results = ordered_clone.limit(by).skip(idx)) && results.any?)
        results.each do |result|
          return self if set_limit and set_limit >= total
          total += 1
          yield result
        end
        idx += by
      end
      self
    end

    # Optimized version of the max aggregate method.
    # It works efficiently only if the field is part of a MongoDB index.
    # more here: http://stackoverflow.com/questions/4762980/getting-the-highest-value-of-a-column-in-mongodb
    def indexed_max(field)
      _criteria = criteria.order_by(field.to_sym.desc).only(field.to_sym)
      selector  = _criteria.send(:selector_with_type_selection)
      fields    = _criteria.options[:fields]
      sort      = _criteria.options[:sort]

      document = collection.find(selector).projection(fields).sort(sort).limit(1).first
      document ? document[field.to_s].to_i : nil
    end

    def to_liquid
      Locomotive::Liquid::Drops::ProxyCollection.new(self)
    end

    private

    def ordered_clone
      options[:sort] ? clone : clone.asc(:_id)
    end
  end

  module Findable
    def indexed_max(field)
      with_default_scope.indexed_max(field)
    end
  end

  module Criterion
    class Selector < Hash
      # for some reason, the store method behaves differently than the []= one, causing regression bugs (query not localized)
      alias :store :[]=
    end
  end

  # make the validators work with localized field
  module Validatable #:nodoc:

    class ExclusionValidator < ActiveModel::Validations::ExclusionValidator
      include Localizable
    end

    module ClassMethods
      def validates_exclusion_of(*args)
        validates_with(ExclusionValidator, _merge_attributes(args))
      end
    end

    module LocalizedEachValidator

      def validate_each(document, attribute, value)
        # validate the value only in the current locale
        if (field = document.fields[document.database_field_name(attribute)]).try(:localized?)
          value = value.try(:slice, ::Mongoid::Fields::I18n.locale.to_s)
        end

        super(document, attribute, value)
      end

    end

    [FormatValidator, LengthValidator, UniquenessValidator, ExclusionValidator].each do |klass|
      klass.send(:include, LocalizedEachValidator)
    end

    # PresenceValidator defines its own validate_each method
    PresenceValidator.send(:prepend, LocalizedEachValidator)
  end

end