module HasScope
TRUE_VALUES = ["true", true, "1", 1]
ALLOWED_TYPES = {
:array => [ Array ],
:hash => [ Hash ],
:default => [ String, Numeric ]
}
def self.included(base)
base.class_eval do
extend ClassMethods
helper_method :current_scopes
class_inheritable_hash :scopes_configuration, :instance_writer => false
self.scopes_configuration ||= {}
end
end
module ClassMethods
# Detects params from url and apply as scopes to your classes.
#
# == Options
#
# * :type - Checks the type of the parameter sent. If set to :boolean
# it just calls the named scope, without any argument. By default,
# it does not allow hashes or arrays to be given, except if type
# :hash or :array are set.
#
# * :only - In which actions the scope is applied. By default is :all.
#
# * :except - In which actions the scope is not applied. By default is :none.
#
# * :as - The key in the params hash expected to find the scope.
# Defaults to the scope name.
#
# * :if - Specifies a method, proc or string to call to determine
# if the scope should apply
#
# * :unless - Specifies a method, proc or string to call to determine
# if the scope should NOT apply.
#
# * :default - Default value for the scope. Whenever supplied the scope
# is always called.
#
def has_scope(*scopes)
options = scopes.extract_options!
options.symbolize_keys!
if options.delete(:boolean)
options[:type] ||= :boolean
ActiveSupport::Deprecation.warn(":boolean => true is deprecated, use :type => :boolean instead", caller)
end
options.assert_valid_keys(:type, :only, :except, :if, :unless, :default, :as)
options[:only] = Array(options[:only])
options[:except] = Array(options[:except])
scopes.each do |scope|
self.scopes_configuration[scope] ||= { :as => scope, :type => :default }
self.scopes_configuration[scope].merge!(options)
end
end
end
protected
# Receives an object where scopes will be applied to.
#
# class GraduationsController < InheritedResources::Base
# has_scope :featured, :boolean => true, :only => :index
# has_scope :by_degree, :only => :index
#
# def index
# @graduations = apply_scopes(Graduation).all
# end
# end
#
def apply_scopes(target)
self.scopes_configuration.each do |scope, options|
next unless apply_scope_to_action?(options)
key = options[:as]
if params.key?(key)
value, call_scope = params[key], true
elsif options.key?(:default)
value, call_scope = options[:default], true
value = value.call(self) if value.is_a?(Proc)
end
target = apply_scope_by_type(options[:type], key, scope, value, target) if call_scope
end
target
end
# Apply the scope taking into account its type.
def apply_scope_by_type(type, key, scope, value, target) #:nodoc:
if type == :boolean
current_scopes[key] = TRUE_VALUES.include?(value)
current_scopes[key] ? target.send(scope) : target
elsif ALLOWED_TYPES[type].none?{ |klass| value.is_a?(klass) }
raise "Expected type :#{type} in params[:#{key}], got :#{value.class}"
else
current_scopes[key] = value
target.send(scope, value)
end
end
# Given an options with :only and :except arrays, check if the scope
# can be performed in the current action.
def apply_scope_to_action?(options) #:nodoc:
return false unless applicable?(options[:if], true) && applicable?(options[:unless], false)
if options[:only].empty?
options[:except].empty? || !options[:except].include?(action_name.to_sym)
else
options[:only].include?(action_name.to_sym)
end
end
# Evaluates the scope options :if or :unless. Returns true if the proc
# method, or string evals to the expected value.
def applicable?(string_proc_or_symbol, expected) #:nodoc:
case string_proc_or_symbol
when String
eval(string_proc_or_symbol) == expected
when Proc
string_proc_or_symbol.call(self) == expected
when Symbol
send(string_proc_or_symbol) == expected
else
true
end
end
# Returns the scopes used in this action.
def current_scopes
@current_scopes ||= {}
end
end
ApplicationController.send :include, HasScope