acts-as-approvable 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/Appraisals +18 -0
- data/CHANGELOG +10 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +50 -0
- data/MIT-LICENSE +20 -0
- data/README.md +108 -0
- data/Rakefile +81 -0
- data/VERSION +1 -0
- data/acts-as-approvable.gemspec +31 -0
- data/gemfiles/rails2.gemfile +7 -0
- data/gemfiles/rails2.gemfile.lock +50 -0
- data/gemfiles/rails30.gemfile +8 -0
- data/gemfiles/rails30.gemfile.lock +90 -0
- data/gemfiles/rails31.gemfile +8 -0
- data/gemfiles/rails31.gemfile.lock +101 -0
- data/gemfiles/rails32.gemfile +8 -0
- data/gemfiles/rails32.gemfile.lock +99 -0
- data/generators/acts_as_approvable/USAGE +3 -0
- data/generators/acts_as_approvable/acts_as_approvable_generator.rb +86 -0
- data/generators/acts_as_approvable/templates/approvals_controller.rb +97 -0
- data/generators/acts_as_approvable/templates/create_approvals.rb +26 -0
- data/generators/acts_as_approvable/templates/initializer.rb +3 -0
- data/generators/acts_as_approvable/templates/views/erb/_owner_select.html.erb +4 -0
- data/generators/acts_as_approvable/templates/views/erb/_table.html.erb +26 -0
- data/generators/acts_as_approvable/templates/views/erb/index.html.erb +15 -0
- data/generators/acts_as_approvable/templates/views/haml/_owner_select.html.haml +3 -0
- data/generators/acts_as_approvable/templates/views/haml/_table.html.haml +19 -0
- data/generators/acts_as_approvable/templates/views/haml/index.html.haml +13 -0
- data/init.rb +1 -0
- data/lib/acts-as-approvable/version.rb +3 -0
- data/lib/acts_as_approvable/acts_as_approvable.rb +291 -0
- data/lib/acts_as_approvable/approval.rb +179 -0
- data/lib/acts_as_approvable/error.rb +31 -0
- data/lib/acts_as_approvable/ownership.rb +117 -0
- data/lib/acts_as_approvable/railtie.rb +7 -0
- data/lib/acts_as_approvable.rb +66 -0
- data/lib/generators/acts_as_approvable/USAGE +1 -0
- data/lib/generators/acts_as_approvable/acts_as_approvable_generator.rb +73 -0
- data/lib/generators/acts_as_approvable/templates/approvals_controller.rb +97 -0
- data/lib/generators/acts_as_approvable/templates/create_approvals.rb +26 -0
- data/lib/generators/acts_as_approvable.rb +0 -0
- data/lib/generators/erb/acts_as_approvable_generator.rb +44 -0
- data/lib/generators/erb/templates/_owner_select.html.erb +4 -0
- data/lib/generators/erb/templates/_table.html.erb +26 -0
- data/lib/generators/erb/templates/index.html.erb +15 -0
- data/lib/generators/haml/acts_as_approvable_generator.rb +44 -0
- data/lib/generators/haml/templates/_owner_select.html.haml +3 -0
- data/lib/generators/haml/templates/_table.html.haml +19 -0
- data/lib/generators/haml/templates/index.html.haml +13 -0
- data/lib/tasks/acts_as_approvable.rake +4 -0
- data/rails/init.rb +1 -0
- data/test/acts_as_approvable_model_test.rb +428 -0
- data/test/acts_as_approvable_ownership_test.rb +132 -0
- data/test/acts_as_approvable_schema_test.rb +13 -0
- data/test/acts_as_approvable_test.rb +8 -0
- data/test/database.yml +7 -0
- data/test/schema.rb +44 -0
- data/test/support.rb +19 -0
- data/test/test_helper.rb +60 -0
- metadata +225 -0
@@ -0,0 +1,291 @@
|
|
1
|
+
module ActsAsApprovable
|
2
|
+
##
|
3
|
+
# The meat of {ActsAsApprovable}. This applies methods for the configured approval events
|
4
|
+
# and configures the required relationships.
|
5
|
+
module Model
|
6
|
+
def self.included(base)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
##
|
11
|
+
# Class methods added to `ActiveRecord::Base`.
|
12
|
+
module ClassMethods
|
13
|
+
# Declare this in your model to require approval on new records or changes to fields.
|
14
|
+
#
|
15
|
+
# @param [Hash] options the options for this models approval workflow.
|
16
|
+
# @option options [Symbol,Array] :on The events to require approval on (`:create` or `:update`).
|
17
|
+
# @option options [String] :state_field The local field to store `:create` approval state.
|
18
|
+
# @option options [Array] :ignore A list of fields to ignore. By default we ignore `:created_at`, `:updated_at` and
|
19
|
+
# the field specified in `:state_field`.
|
20
|
+
# @option options [Array] :only A list of fields to explicitly require approval on. This list supercedes `:ignore`.
|
21
|
+
def acts_as_approvable(options = {})
|
22
|
+
include InstanceMethods
|
23
|
+
|
24
|
+
cattr_accessor :approvable_on
|
25
|
+
self.approvable_on = Array.wrap(options.delete(:on) { [:create, :update] })
|
26
|
+
|
27
|
+
cattr_accessor :approvable_field
|
28
|
+
self.approvable_field = options.delete(:state_field)
|
29
|
+
|
30
|
+
cattr_accessor :approvable_ignore
|
31
|
+
ignores = Array.wrap(options.delete(:ignore) { [] })
|
32
|
+
ignores.push('created_at', 'updated_at', self.approvable_field)
|
33
|
+
self.approvable_ignore = ignores.compact.uniq.map(&:to_s)
|
34
|
+
|
35
|
+
cattr_accessor :approvable_only
|
36
|
+
self.approvable_only = Array.wrap(options.delete(:only) { [] }).uniq.map(&:to_s)
|
37
|
+
|
38
|
+
cattr_accessor :approvals_active
|
39
|
+
self.approvals_active = true
|
40
|
+
|
41
|
+
has_many :approvals, :as => :item, :dependent => :destroy
|
42
|
+
|
43
|
+
if self.approvable_on.include?(:update)
|
44
|
+
include UpdateInstanceMethods
|
45
|
+
before_update :approvable_update, :if => :approvable_update?
|
46
|
+
end
|
47
|
+
|
48
|
+
if self.approvable_on.include?(:create)
|
49
|
+
include CreateInstanceMethods
|
50
|
+
before_create :approvable_create, :if => :approvable_create?
|
51
|
+
end
|
52
|
+
|
53
|
+
after_save :approvable_save, :if => :approvals_enabled?
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Enable the approval queue for this model.
|
58
|
+
def approvals_on
|
59
|
+
self.approvals_active = true
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Disable the approval queue for this model.
|
64
|
+
def approvals_off
|
65
|
+
self.approvals_active = false
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Execute a code block while the approval queue is temporarily disabled. The
|
70
|
+
# queue state will be returned to it's previous value, either on or off.
|
71
|
+
def without_approval(&block)
|
72
|
+
enable = self.approvals_active
|
73
|
+
approvals_off
|
74
|
+
yield(self)
|
75
|
+
ensure
|
76
|
+
approvals_on if enable
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Instance methods that apply to the `:create` event specifically.
|
82
|
+
module CreateInstanceMethods
|
83
|
+
##
|
84
|
+
# Retrieve approval record for the creation event.
|
85
|
+
#
|
86
|
+
# @return [Approval]
|
87
|
+
def approval
|
88
|
+
approvals.find_by_event('create')
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Get the approval state of the current record from either the local state
|
93
|
+
# field or, if no state field exists, the creation approval object.
|
94
|
+
#
|
95
|
+
# @return [String] one of `'pending'`, `'approved`' or `'rejected'`.
|
96
|
+
def approval_state
|
97
|
+
if self.class.approvable_field
|
98
|
+
send(self.class.approvable_field)
|
99
|
+
else
|
100
|
+
approval.state
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Set the records local approval state.
|
106
|
+
#
|
107
|
+
# @param [String] state one of `'pending'`, `'approved`' or `'rejected'`.
|
108
|
+
def set_approval_state(state)
|
109
|
+
return unless self.class.approvable_field
|
110
|
+
send("#{self.class.approvable_field}=".to_sym, state)
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Returns true if the record is pending approval.
|
115
|
+
def pending?
|
116
|
+
approval_state == 'pending' or approval.present? and !approved? and !rejected?
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# Returns true if the record has been approved.
|
121
|
+
def approved?
|
122
|
+
approval_state == 'approved' or approval.nil? or approval.approved?
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# Returns true if the record has been rejected.
|
127
|
+
def rejected?
|
128
|
+
approval_state == 'rejected' or approval.present? and approval.rejected?
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Approves the record through {Approval#approve!}
|
133
|
+
#
|
134
|
+
# @return [Boolean]
|
135
|
+
def approve!
|
136
|
+
return unless approvable_on?(:create) && approval.present?
|
137
|
+
approval.approve!
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Rejects the record through {Approval#reject!}
|
142
|
+
#
|
143
|
+
# @return [Boolean]
|
144
|
+
def reject!
|
145
|
+
return unless approvable_on?(:create) && approval.present?
|
146
|
+
approval.reject!
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
def approvable_create?
|
151
|
+
approvals_enabled? and approvable_on?(:create)
|
152
|
+
end
|
153
|
+
|
154
|
+
def approvable_create
|
155
|
+
@approval = approvals.build(:event => 'create', :state => 'pending')
|
156
|
+
set_approval_state('pending')
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Instance methods that apply to the :update event specifically.
|
162
|
+
module UpdateInstanceMethods
|
163
|
+
##
|
164
|
+
# Retrieve all approval records for `:update` events.
|
165
|
+
def update_approvals
|
166
|
+
approvals.find_all_by_event('update')
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Returns true if the record has any `#update_approvals` that are pending
|
171
|
+
# approval.
|
172
|
+
def pending_changes?
|
173
|
+
!update_approvals.empty?
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Returns true if any notable (eg. not ignored) fields have been changed.
|
178
|
+
def changed_notably?
|
179
|
+
notably_changed.any?
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Returns an array of any notable (eg. not ignored) fields that have not
|
184
|
+
# been changed.
|
185
|
+
#
|
186
|
+
# @return [Array] a list of changed fields.
|
187
|
+
def notably_changed
|
188
|
+
unless self.class.approvable_only.empty?
|
189
|
+
self.class.approvable_only.select { |field| changed.include?(field) }
|
190
|
+
else
|
191
|
+
changed - self.class.approvable_ignore
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
def approvable_update?
|
197
|
+
approvals_enabled? and approvable_on?(:update) and changed_notably?
|
198
|
+
end
|
199
|
+
|
200
|
+
def approvable_update
|
201
|
+
changed = {}
|
202
|
+
notably_changed.each do |attr|
|
203
|
+
original, changed_to = changes[attr]
|
204
|
+
|
205
|
+
write_attribute(attr.to_s, original)
|
206
|
+
changed[attr] = changed_to
|
207
|
+
end
|
208
|
+
|
209
|
+
@approval = approvals.build(:event => 'update', :state => 'pending', :object => changed)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
##
|
214
|
+
# Instance methods that apply to both `:update` and `:create` events.
|
215
|
+
module InstanceMethods
|
216
|
+
##
|
217
|
+
# Returns true if the approval queue is active at both the local and global
|
218
|
+
# level. Note that the global level supercedes the local level.
|
219
|
+
def approvals_enabled?
|
220
|
+
ActsAsApprovable.enabled? and self.class.approvals_active and approvals_on?
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Returns the inverse of `#approvals_enabled?`
|
225
|
+
def approvals_disabled?
|
226
|
+
!approvals_enabled?
|
227
|
+
end
|
228
|
+
|
229
|
+
def approvals_off
|
230
|
+
@approvals_disabled = true
|
231
|
+
end
|
232
|
+
|
233
|
+
def approvals_on
|
234
|
+
@approvals_disabled = false
|
235
|
+
end
|
236
|
+
|
237
|
+
def approvals_on?
|
238
|
+
not @approvals_disabled
|
239
|
+
end
|
240
|
+
|
241
|
+
##
|
242
|
+
# Returns true if the model is configured to use the approval queue on the
|
243
|
+
# given event (`:create` or `:update`).
|
244
|
+
def approvable_on?(event)
|
245
|
+
self.class.approvable_on.include?(event)
|
246
|
+
end
|
247
|
+
|
248
|
+
##
|
249
|
+
# A filter that is run before the record can be approved. Returning false
|
250
|
+
# stops the approval process from completing.
|
251
|
+
def before_approve(approval); end
|
252
|
+
|
253
|
+
##
|
254
|
+
# A filter that is run after the record has been approved.
|
255
|
+
def after_approve(approval); end
|
256
|
+
|
257
|
+
##
|
258
|
+
# A filter that is run before the record can be rejected. Returning false
|
259
|
+
# stops the rejection process from completing.
|
260
|
+
def before_reject(approval); end
|
261
|
+
|
262
|
+
##
|
263
|
+
# A filter that is run after the record has been rejected.
|
264
|
+
def after_reject(approval); end
|
265
|
+
|
266
|
+
##
|
267
|
+
# Execute a code block while the approval queue is temporarily disabled. The
|
268
|
+
# queue state will be returned to it's previous value, either on or off.
|
269
|
+
def without_approval(&block)
|
270
|
+
enable = approvals_on? # If we use #approvals_enabled? the global state might be incorrectly applied.
|
271
|
+
approvals_off
|
272
|
+
yield(self)
|
273
|
+
ensure
|
274
|
+
approvals_on if enable
|
275
|
+
end
|
276
|
+
|
277
|
+
def save_without_approval(*args)
|
278
|
+
without_approval { |i| save(*args) }
|
279
|
+
end
|
280
|
+
|
281
|
+
def save_without_approval!(*args)
|
282
|
+
without_approval { |i| save!(*args) }
|
283
|
+
end
|
284
|
+
|
285
|
+
private
|
286
|
+
def approvable_save
|
287
|
+
@approval.save if @approval.present? && @approval.new_record?
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
class Approval < ActiveRecord::Base
|
2
|
+
##
|
3
|
+
# Enumeration of available states.
|
4
|
+
STATES = %w(pending approved rejected)
|
5
|
+
|
6
|
+
belongs_to :item, :polymorphic => true
|
7
|
+
|
8
|
+
validates_presence_of :item
|
9
|
+
validates_inclusion_of :event, :in => %w(create update)
|
10
|
+
validates_numericality_of :state, :greater_than_or_equal_to => 0, :less_than => STATES.length
|
11
|
+
|
12
|
+
serialize :object
|
13
|
+
|
14
|
+
before_save :can_save?
|
15
|
+
|
16
|
+
##
|
17
|
+
# Find the enumerated value for a given state.
|
18
|
+
#
|
19
|
+
# @return [Integer]
|
20
|
+
def self.enumerate_state(state)
|
21
|
+
enumerate_states(state).first
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Find the enumerated values for a list of states.
|
26
|
+
#
|
27
|
+
# @return [Array]
|
28
|
+
def self.enumerate_states(*states)
|
29
|
+
states.map { |name| STATES.index(name) }.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Build an array of states usable by Rails' `#options_for_select`.
|
34
|
+
def self.options_for_state
|
35
|
+
options = [['All', -1]]
|
36
|
+
STATES.each_index { |x| options << [STATES[x].capitalize, x] }
|
37
|
+
options
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Build an array of types usable by Rails' `#options_for_select`.
|
42
|
+
def self.options_for_type(with_prompt = false)
|
43
|
+
types = all(:select => 'DISTINCT(item_type)').map { |row| row.item_type }
|
44
|
+
types.unshift(['All Types', nil]) if with_prompt
|
45
|
+
types
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Get the current state of the approval. Converts from integer via {STATES} constant.
|
50
|
+
def state
|
51
|
+
STATES[(read_attribute(:state) || 0)]
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Get the previous state of the approval. Converts from integer via {STATES} constant.
|
56
|
+
def state_was
|
57
|
+
STATES[(changed_attributes[:state] || 0)]
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Set the state of the approval. Converts from string to integer via {STATES} constant.
|
62
|
+
def state=(state)
|
63
|
+
state = self.class.enumerate_state(state) if state.is_a?(String)
|
64
|
+
write_attribute(:state, state)
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Returns true if the approval is still pending.
|
69
|
+
def pending?
|
70
|
+
state == 'pending'
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Returns true if the approval has been approved.
|
75
|
+
def approved?
|
76
|
+
state == 'approved'
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Returns true if the approval has been rejected.
|
81
|
+
def rejected?
|
82
|
+
state == 'rejected'
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Returns true if the approval has been approved or rejected.
|
87
|
+
def locked?
|
88
|
+
approved? or rejected?
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Returns true if the approval has not been approved or rejected.
|
93
|
+
def unlocked?
|
94
|
+
not locked?
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Returns true if the approval able to be saved. This requires an unlocked
|
99
|
+
# approval, or an approval just leaving the 'pending' state.
|
100
|
+
def can_save?
|
101
|
+
unlocked? or state_was == 'pending'
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Returns true if the affected item has been updated since this approval was
|
106
|
+
# created.
|
107
|
+
def stale?
|
108
|
+
unlocked? and item.has_attribute?(:updated_at) and created_at < item.updated_at
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Returns true if the affected item has not been updated since this approval
|
113
|
+
# was created.
|
114
|
+
def fresh?
|
115
|
+
not stale?
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Returns true if this is an `:update` approval event.
|
120
|
+
def update?
|
121
|
+
event == 'update'
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Returns true if this is a `:create` approval event.
|
126
|
+
def create?
|
127
|
+
event == 'create'
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Attempt to approve the record change.
|
132
|
+
#
|
133
|
+
# @param [Boolean] force if the approval record is stale force the acceptance.
|
134
|
+
# @raise [ActsAsApprovable::Error::Locked] raised if the record is {#locked? locked}.
|
135
|
+
# @raise [ActsAsApprovable::Error::Stale] raised if the record is {#stale? stale} and `force` is false.
|
136
|
+
def approve!(force = false)
|
137
|
+
raise ActsAsApprovable::Error::Locked if locked?
|
138
|
+
raise ActsAsApprovable::Error::Stale if stale? and !force
|
139
|
+
return unless run_item_callback(:before_approve)
|
140
|
+
|
141
|
+
if update?
|
142
|
+
data = {}
|
143
|
+
object.each do |attr, value|
|
144
|
+
data[attr] = value if item.attribute_names.include?(attr)
|
145
|
+
end
|
146
|
+
|
147
|
+
item.attributes = data
|
148
|
+
elsif create?
|
149
|
+
item.set_approval_state('approved')
|
150
|
+
end
|
151
|
+
|
152
|
+
item.save_without_approval!
|
153
|
+
update_attributes!(:state => 'approved')
|
154
|
+
run_item_callback(:after_approve)
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Attempt to reject the record change.
|
159
|
+
#
|
160
|
+
# @param [String] reason a reason for rejecting the change.
|
161
|
+
# @raise [ActsAsApprovable::Error::Locked] raised if the record is {#locked? locked}.
|
162
|
+
def reject!(reason = nil)
|
163
|
+
raise ActsAsApprovable::Error::Locked if locked?
|
164
|
+
return unless run_item_callback(:before_reject)
|
165
|
+
|
166
|
+
if create?
|
167
|
+
item.set_approval_state('rejected')
|
168
|
+
end
|
169
|
+
|
170
|
+
item.save_without_approval!
|
171
|
+
update_attributes!(:state => 'rejected', :reason => reason)
|
172
|
+
run_item_callback(:after_reject)
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
def run_item_callback(callback)
|
177
|
+
item.send(callback, self) != false
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ActsAsApprovable
|
2
|
+
class Error < RuntimeError
|
3
|
+
##
|
4
|
+
# Raised when a locked approval is accepted or rejected.
|
5
|
+
class Locked < ActsAsApprovable::Error
|
6
|
+
def initialize(*args)
|
7
|
+
args[0] = 'this approval is locked'
|
8
|
+
super(*args)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
##
|
13
|
+
# Raised when a stale approval is accepted.
|
14
|
+
class Stale < ActsAsApprovable::Error
|
15
|
+
def initialize(*args)
|
16
|
+
args[0] = 'this approval is stale and should not be approved'
|
17
|
+
super(*args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Raised when a record is assigned as owner that is not found in
|
23
|
+
# {ActsAsApprovable::Ownership::ClassMethods#available_owners}.
|
24
|
+
class InvalidOwner < ActsAsApprovable::Error
|
25
|
+
def initialize(*args)
|
26
|
+
args[0] = 'this record cannot be assigned as an owner'
|
27
|
+
super(*args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module ActsAsApprovable
|
2
|
+
##
|
3
|
+
# This module provides the {Approval} class with the ability to assign records
|
4
|
+
# as an "owner" of the approval. This is especially useful for tracking purposes
|
5
|
+
# when you require it, and can be beneficial when you have an approval queue with
|
6
|
+
# a high rate of insertions.
|
7
|
+
#
|
8
|
+
# By default the ownership functionality will reference a model named `User` and
|
9
|
+
# will allow any user to take ownership of an approval.
|
10
|
+
module Ownership
|
11
|
+
##
|
12
|
+
# Configure approvals to allow ownership by a User model.
|
13
|
+
#
|
14
|
+
# If a block is given it will be applied to Approval at the class level,
|
15
|
+
# allowing you to override functionality on the fly.
|
16
|
+
#
|
17
|
+
# @param [Hash] options a hash of options for configuration
|
18
|
+
# @option options [Object] :model the model being used for Approval records (defaults to `Approval`).
|
19
|
+
# @option options [Object] :owner the model being used for owner records (defaults to `User`).
|
20
|
+
def self.configure(options = {}, &block)
|
21
|
+
approval = options.delete(:model) { Approval }
|
22
|
+
owner = options.delete(:owner) { User }
|
23
|
+
|
24
|
+
approval.send(:include, self)
|
25
|
+
|
26
|
+
ActsAsApprovable.owner_class = owner
|
27
|
+
approval.send(:belongs_to, :owner, :class_name => owner.to_s, :foreign_key => :owner_id)
|
28
|
+
|
29
|
+
approval.class_exec(&block) if block
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.included(base)
|
33
|
+
base.send(:include, InstanceMethods)
|
34
|
+
base.extend(ClassMethods)
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Instance methods for approval ownership.
|
39
|
+
module InstanceMethods
|
40
|
+
##
|
41
|
+
# Set the owner and save the record.
|
42
|
+
#
|
43
|
+
# @return [Boolean]
|
44
|
+
def assign(owner)
|
45
|
+
raise ActsAsApprovable::Error::InvalidOwner unless self.class.available_owners.include?(owner)
|
46
|
+
self.owner = owner
|
47
|
+
save
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Removed any assigned owner and save the record.
|
52
|
+
#
|
53
|
+
# @return [Boolean]
|
54
|
+
def unassign
|
55
|
+
self.owner = nil
|
56
|
+
save
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Class methods for approval ownership.
|
62
|
+
module ClassMethods
|
63
|
+
##
|
64
|
+
# Get the model that represents an owner.
|
65
|
+
#
|
66
|
+
# @see ActsAsApprovable::Ownership.configure
|
67
|
+
def owner_class
|
68
|
+
ActsAsApprovable.owner_class
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# A list of records that can be assigned to an approval. This should be
|
73
|
+
# overridden in {ActsAsApprovable::Ownership.configure} to return only the
|
74
|
+
# records you wish to manage approvals.
|
75
|
+
def available_owners
|
76
|
+
owner_class.all
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Build an array from {#available_owners} usable by Rails' `#options_for_select`.
|
81
|
+
# Each element in the array is built with {#option_for_owner}.
|
82
|
+
#
|
83
|
+
# @return [Array]
|
84
|
+
def options_for_available_owners(with_prompt = false)
|
85
|
+
owners = available_owners.map { |owner| option_for_owner(owner) }
|
86
|
+
owners.unshift(['(none)', nil]) if with_prompt
|
87
|
+
owners
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# A list of owners that have assigned approvals.
|
92
|
+
def assigned_owners
|
93
|
+
all(:select => 'DISTINCT(owner_id)', :conditions => 'owner_id IS NOT NULL', :include => :owner).map(&:owner)
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Build an array from {#assigned_owners} usable by Rails' `#options_for_select`.
|
98
|
+
# Each element in the array is built with {#option_for_owner}.
|
99
|
+
#
|
100
|
+
# @return [Array]
|
101
|
+
def options_for_assigned_owners(with_prompt = false)
|
102
|
+
owners = assigned_owners.map { |owner| option_for_owner(owner) }
|
103
|
+
owners.unshift(['All Users', nil]) if with_prompt
|
104
|
+
owners
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Helper method that takes an owner record and returns an array for Rails'
|
109
|
+
# `#options_for_select`.
|
110
|
+
#
|
111
|
+
# @return [Array] a 2-index array with a display string and value.
|
112
|
+
def option_for_owner(owner)
|
113
|
+
[owner.to_str, owner.id]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
require 'acts_as_approvable/acts_as_approvable'
|
4
|
+
require 'acts_as_approvable/approval'
|
5
|
+
require 'acts_as_approvable/error'
|
6
|
+
require 'acts_as_approvable/ownership'
|
7
|
+
require 'acts-as-approvable/version'
|
8
|
+
|
9
|
+
if defined?(Rails) && Rails.version =~ /^3\./
|
10
|
+
require 'acts_as_approvable/railtie'
|
11
|
+
elsif defined?(ActiveRecord)
|
12
|
+
ActiveRecord::Base.send :include, ActsAsApprovable::Model
|
13
|
+
end
|
14
|
+
|
15
|
+
module ActsAsApprovable
|
16
|
+
##
|
17
|
+
# Enable the approval queue at a global level.
|
18
|
+
def self.enable
|
19
|
+
@enabled = true
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Disable the approval queue at a global level.
|
24
|
+
def self.disable
|
25
|
+
@enabled = false
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Returns true if the approval queue is enabled globally.
|
30
|
+
def self.enabled?
|
31
|
+
@enabled = true if @enabled.nil?
|
32
|
+
@enabled
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Set the referenced Owner class to be used by generic finders.
|
37
|
+
#
|
38
|
+
# @see Ownership
|
39
|
+
def self.owner_class=(klass)
|
40
|
+
@owner_class = klass
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Get the referenced Owner class to be used by generic finders.
|
45
|
+
#
|
46
|
+
# @see Ownership
|
47
|
+
def self.owner_class
|
48
|
+
@owner_class
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Set the engine used for rendering view files.
|
53
|
+
def self.view_language=(lang)
|
54
|
+
@lang = lang
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Get the engine used for rendering view files. Defaults to 'erb'
|
59
|
+
def self.view_language
|
60
|
+
if Rails.version =~ /^3\./
|
61
|
+
Rails.configuration.generators.rails[:template_engine].try(:to_s) || 'erb'
|
62
|
+
else
|
63
|
+
@lang || 'erb'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Generates ApprovalsController, a migration the create the Approval table, and an initializer for the plugin.
|