1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
|
require 'active_support'
require 'action_controller'
module HasScope
TRUE_VALUES = ["true", true, "1", 1]
ALLOWED_TYPES = {
array: [[ Array ]],
hash: [[ Hash, ActionController::Parameters ]],
boolean: [[ Object ], -> v { TRUE_VALUES.include?(v) }],
default: [[ String, Numeric ]],
}
def self.deprecator
@deprecator ||= ActiveSupport::Deprecation.new("1.0", "HasScope")
end
def self.included(base)
base.class_eval do
extend ClassMethods
class_attribute :scopes_configuration, instance_writer: false
self.scopes_configuration = {}
end
end
module ClassMethods
# Detects params from url and apply as scopes to your classes.
#
# == Options
#
# * <tt>:type</tt> - 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.
#
# * <tt>:only</tt> - In which actions the scope is applied. By default is :all.
#
# * <tt>:except</tt> - In which actions the scope is not applied. By default is :none.
#
# * <tt>:as</tt> - The key in the params hash expected to find the scope.
# Defaults to the scope name.
#
# * <tt>:using</tt> - If type is a hash, you can provide :using to convert the hash to
# a named scope call with several arguments.
#
# * <tt>:in</tt> - A shortcut for combining the `:using` option with nested hashes.
#
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
# if the scope should apply
#
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
# if the scope should NOT apply.
#
# * <tt>:default</tt> - Default value for the scope. Whenever supplied the scope
# is always called.
#
# * <tt>:allow_blank</tt> - Blank values are not sent to scopes by default. Set to true to overwrite.
#
# == Block usage
#
# has_scope also accepts a block. The controller, current scope and value are yielded
# to the block so the user can apply the scope on its own. This is useful in case we
# need to manipulate the given value:
#
# has_scope :category do |controller, scope, value|
# value != "all" ? scope.by_category(value) : scope
# end
#
# has_scope :not_voted_by_me, type: :boolean do |controller, scope|
# scope.not_voted_by(controller.current_user.id)
# end
#
def has_scope(*scopes, &block)
options = scopes.extract_options!
options.symbolize_keys!
options.assert_valid_keys(:type, :only, :except, :if, :unless, :default, :as, :using, :allow_blank, :in)
if options.key?(:in)
options[:as] = options[:in]
options[:using] = scopes
if options.key?(:default) && !options[:default].is_a?(Hash)
options[:default] = scopes.each_with_object({}) { |scope, hash| hash[scope] = options[:default] }
end
end
if options.key?(:using)
if options.key?(:type) && options[:type] != :hash
raise "You cannot use :using with another :type different than :hash"
else
options[:type] = :hash
end
options[:using] = Array(options[:using])
end
options[:only] = Array(options[:only])
options[:except] = Array(options[:except])
self.scopes_configuration = scopes_configuration.dup
scopes.each do |scope|
scopes_configuration[scope] ||= { as: scope, type: :default, block: block }
scopes_configuration[scope] = self.scopes_configuration[scope].merge(options)
end
end
end
protected
# Receives an object where scopes will be applied to.
#
# class GraduationsController < ApplicationController
# has_scope :featured, type: true, only: :index
# has_scope :by_degree, only: :index
#
# def index
# @graduations = apply_scopes(Graduation).all
# end
# end
#
def apply_scopes(target, hash = params)
scopes_configuration.each do |scope, options|
next unless apply_scope_to_action?(options)
key = options[:as]
if hash.key?(key)
value, call_scope = hash[key], true
elsif options.key?(:default)
value, call_scope = options[:default], true
if value.is_a?(Proc)
value = value.arity == 0 ? value.call : value.call(self)
end
end
value = parse_value(options[:type], value)
value = normalize_blanks(value)
if value && options.key?(:using)
scope_value = value.values_at(*options[:using])
call_scope &&= scope_value.all?(&:present?) || options[:allow_blank]
else
scope_value = value
call_scope &&= value.present? || options[:allow_blank]
end
if call_scope
current_scopes[key] = value
target = call_scope_by_type(options[:type], scope, target, scope_value, options)
end
end
target
end
# Set the real value for the current scope if type check.
def parse_value(type, value) #:nodoc:
klasses, parser = ALLOWED_TYPES[type]
if klasses.any? { |klass| value.is_a?(klass) }
parser ? parser.call(value) : value
end
end
# Screens pseudo-blank params.
def normalize_blanks(value) #:nodoc:
case value
when Array
value.select { |v| v.present? }
when Hash
value.select { |k, v| normalize_blanks(v).present? }.with_indifferent_access
when ActionController::Parameters
normalize_blanks(value.to_unsafe_h)
else
value
end
end
# Call the scope taking into account its type.
def call_scope_by_type(type, scope, target, value, options) #:nodoc:
block = options[:block]
if type == :boolean && !options[:allow_blank]
block ? block.call(self, target) : target.send(scope)
elsif options.key?(:using)
block ? block.call(self, target, value) : target.send(scope, *value)
else
block ? block.call(self, target, 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
HasScope.deprecator.warn <<-DEPRECATION.squish
[HasScope] Passing a string to determine if the scope should be applied
is deprecated and it will be removed in a future version of HasScope.
DEPRECATION
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
require 'has_scope/railtie' if defined?(Rails)
ActiveSupport.on_load :action_controller do
include HasScope
helper_method :current_scopes if respond_to?(:helper_method)
end
|