lib/mongoid/scopable.rb
# encoding: utf-8
module Mongoid
# This module contains behaviour for all Mongoid scoping - named scopes,
# default scopes, and criteria accessors via scoped and unscoped.
#
# @since 4.0.0
module Scopable
extend ActiveSupport::Concern
included do
class_attribute :default_scoping
class_attribute :_declared_scopes
self._declared_scopes = {}
end
private
# Apply the default scoping to the attributes of the document, as long as
# they are not complex queries.
#
# @api private
#
# @example Apply the default scoping.
# document.apply_default_scoping
#
# @return [ true, false ] If default scoping was applied.
#
# @since 4.0.0
def apply_default_scoping
if default_scoping
default_scoping.call.selector.each do |field, value|
attributes[field] = value unless value.respond_to?(:each)
end
end
end
module ClassMethods
# Returns a hash of all the scopes defined for this class, including
# scopes defined on ancestor classes.
#
# @example Get the defined scopes for a class
# class Band
# include Mongoid::Document
# field :active, type: Boolean
#
# scope :active, -> { where(active: true) }
# end
# Band.scopes
#
# @return [ Hash ] The scopes defined for this class
#
# @since 3.1.4
def scopes
defined_scopes = {}
ancestors.reverse.each do |klass|
if klass.respond_to?(:_declared_scopes)
defined_scopes.merge!(klass._declared_scopes)
end
end
defined_scopes.freeze
end
# Add a default scope to the model. This scope will be applied to all
# criteria unless #unscoped is specified.
#
# @example Define a default scope with a criteria.
# class Band
# include Mongoid::Document
# field :active, type: Boolean
# default_scope where(active: true)
# end
#
# @example Define a default scope with a proc.
# class Band
# include Mongoid::Document
# field :active, type: Boolean
# default_scope ->{ where(active: true) }
# end
#
# @param [ Proc, Criteria ] scope The default scope.
#
# @raise [ Errors::InvalidScope ] If the scope is not a proc or criteria.
#
# @return [ Proc ] The default scope.
#
# @since 1.0.0
def default_scope(value)
check_scope_validity(value)
self.default_scoping = process_default_scope(value)
end
# Is the class able to have the default scope applied?
#
# @example Can the default scope be applied?
# Band.default_scopable?
#
# @return [ true, false ] If the default scope can be applied.
#
# @since 3.0.0
def default_scopable?
default_scoping? && !Threaded.executing?(:without_default_scope)
end
# Get a queryable, either the last one on the scope stack or a fresh one.
#
# @api private
#
# @example Get a queryable.
# Model.queryable
#
# @return [ Criteria ] The queryable.
#
# @since 3.0.0
def queryable
Threaded.current_scope || Criteria.new(self)
end
# Create a scope that can be accessed from the class level or chained to
# criteria by the provided name.
#
# @example Create named scopes.
#
# class Person
# include Mongoid::Document
# field :active, type: Boolean
# field :count, type: Integer
#
# scope :active, -> { where(active: true) }
# scope :at_least, ->(count){ where(:count.gt => count) }
# end
#
# @param [ Symbol ] name The name of the scope.
# @param [ Proc ] conditions The conditions of the scope.
#
# @raise [ Errors::InvalidScope ] If the scope is not a proc.
# @raise [ Errors::ScopeOverwrite ] If the scope name already exists.
#
# @since 1.0.0
def scope(name, value, &block)
normalized = name.to_sym
check_scope_validity(value)
check_scope_name(normalized)
_declared_scopes[normalized] = {
scope: value,
extension: Module.new(&block)
}
define_scope_method(normalized)
end
# Get a criteria for the document with normal scoping.
#
# @example Get the criteria.
# Band.scoped(skip: 10)
#
# @note This will force the default scope to be applied.
#
# @param [ Hash ] options Query options for the criteria.
#
# @option options [ Integer ] :skip Optional number of documents to skip.
# @option options [ Integer ] :limit Optional number of documents to
# limit.
# @option options [ Array ] :sort Optional sorting options.
#
# @return [ Criteria ] A scoped criteria.
#
# @since 3.0.0
def scoped(options = nil)
queryable.scoped(options)
end
# Get the criteria without the default scoping applied.
#
# @example Get the unscoped criteria.
# Band.unscoped
#
# @example Yield to block with no default scoping.
# Band.unscoped do
# Band.where(name: "Depeche Mode")
# end
#
# @note This will force the default scope to be removed.
#
# @return [ Criteria, Object ] The unscoped criteria or result of the
# block.
#
# @since 3.0.0
def unscoped
if block_given?
without_default_scope do
yield(self)
end
else
queryable.unscoped
end
end
# Get a criteria with the default scope applied, if possible.
#
# @example Get a criteria with the default scope.
# Model.with_default_scope
#
# @return [ Criteria ] The criteria.
#
# @since 3.0.0
def with_default_scope
queryable.with_default_scope
end
alias :criteria :with_default_scope
# Pushes the provided criteria onto the scope stack, and removes it after the
# provided block is yielded.
#
# @example Yield to the criteria.
# Person.with_scope(criteria)
#
# @param [ Criteria ] criteria The criteria to apply.
#
# @return [ Criteria ] The yielded criteria.
#
# @since 1.0.0
def with_scope(criteria)
Threaded.current_scope = criteria
begin
yield criteria
ensure
Threaded.current_scope = nil
end
end
# Execute the block without applying the default scope.
#
# @example Execute without the default scope.
# Band.without_default_scope do
# Band.where(name: "Depeche Mode")
# end
#
# @return [ Object ] The result of the block.
#
# @since 3.0.0
def without_default_scope
Threaded.begin_execution("without_default_scope")
yield
ensure
Threaded.exit_execution("without_default_scope")
end
private
# Warns or raises exception if overriding another scope or method.
#
# @api private
#
# @example Warn or raise error if name exists.
# Model.valid_scope_name?("test")
#
# @param [ String, Symbol ] name The name of the scope.
#
# @raise [ Errors::ScopeOverwrite ] If the name exists and configured to
# raise the error.
#
# @since 2.1.0
def check_scope_name(name)
if _declared_scopes[name] || respond_to?(name, true)
if Mongoid.scope_overwrite_exception
raise Errors::ScopeOverwrite.new(self.name, name)
else
if Mongoid.logger
Mongoid.logger.warn(
"Creating scope :#{name}. " +
"Overwriting existing method #{self.name}.#{name}."
)
end
end
end
end
# Checks if the intended scope is a valid object, either a criteria or
# proc with a criteria.
#
# @api private
#
# @example Check if the scope is valid.
# Model.check_scope_validity({})
#
# @param [ Object ] value The intended scope.
#
# @raise [ Errors::InvalidScope ] If the scope is not a valid object.
#
# @since 3.0.0
def check_scope_validity(value)
unless value.respond_to?(:call)
raise Errors::InvalidScope.new(self, value)
end
end
# Defines the actual class method that will execute the scope when
# called.
#
# @api private
#
# @example Define the scope class method.
# Model.define_scope_method(:active)
#
# @param [ Symbol ] name The method/scope name.
#
# @return [ Method ] The defined method.
#
# @since 3.0.0
def define_scope_method(name)
singleton_class.class_eval do
define_method name do |*args|
scoping = _declared_scopes[name]
scope = instance_exec(*args, &scoping[:scope])
extension = scoping[:extension]
criteria = with_default_scope.merge(scope || queryable)
criteria.extend(extension)
criteria
end
end
end
# Process the default scope value. If one already exists, we merge the
# new one into the old one.
#
# @api private
#
# @example Process the default scope.
# Model.process_default_scope(value)
#
# @param [ Criteria, Proc ] value The default scope value.
#
# @since 3.0.5
def process_default_scope(value)
if existing = default_scoping
->{ existing.call.merge(value.to_proc.call) }
else
value.to_proc
end
end
end
end
end