/
has_scope.rb
234 lines (204 loc) · 7.48 KB
/
has_scope.rb
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