acts-as-approvable 0.6.7 → 0.6.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/.gitignore +2 -5
  2. data/.rspec +2 -0
  3. data/.travis.yml +5 -0
  4. data/Appraisals +14 -10
  5. data/CHANGELOG +16 -0
  6. data/Gemfile.lock +60 -20
  7. data/README.md +1 -2
  8. data/Rakefile +44 -16
  9. data/VERSION +1 -1
  10. data/acts-as-approvable.gemspec +21 -11
  11. data/features/create_approval.feature +26 -0
  12. data/features/step_definitions/cucumber_steps.rb +86 -0
  13. data/features/support/env.rb +14 -0
  14. data/features/support/large.txt +29943 -0
  15. data/features/support/second_large.txt +31798 -0
  16. data/features/update_approval.feature +34 -0
  17. data/gemfiles/Gemfile.ci +14 -0
  18. data/gemfiles/Gemfile.ci.lock +98 -0
  19. data/gemfiles/mysql2.gemfile +7 -0
  20. data/gemfiles/mysql2.gemfile.lock +92 -0
  21. data/gemfiles/rails2.gemfile +1 -0
  22. data/gemfiles/rails2.gemfile.lock +59 -17
  23. data/gemfiles/rails30.gemfile +1 -0
  24. data/gemfiles/rails30.gemfile.lock +54 -14
  25. data/gemfiles/rails31.gemfile +1 -0
  26. data/gemfiles/rails31.gemfile.lock +54 -14
  27. data/gemfiles/sqlite.gemfile +7 -0
  28. data/generators/acts_as_approvable/templates/create_approvals.rb +8 -7
  29. data/lib/acts-as-approvable.rb +2 -3
  30. data/lib/acts_as_approvable/approval.rb +3 -2
  31. data/lib/acts_as_approvable/model.rb +55 -0
  32. data/lib/acts_as_approvable/model/class_methods.rb +63 -0
  33. data/lib/acts_as_approvable/model/create_instance_methods.rb +83 -0
  34. data/lib/acts_as_approvable/model/instance_methods.rb +89 -0
  35. data/lib/acts_as_approvable/model/update_instance_methods.rb +61 -0
  36. data/lib/acts_as_approvable/railtie.rb +1 -1
  37. data/lib/generators/acts_as_approvable/templates/create_approvals.rb +8 -7
  38. data/spec/acts_as_approvable/approval_spec.rb +475 -0
  39. data/spec/acts_as_approvable/model/class_methods_spec.rb +219 -0
  40. data/spec/acts_as_approvable/model/create_instance_methods_spec.rb +149 -0
  41. data/spec/acts_as_approvable/model/instance_methods_spec.rb +328 -0
  42. data/spec/acts_as_approvable/model/update_instance_methods_spec.rb +111 -0
  43. data/spec/acts_as_approvable/model_spec.rb +90 -0
  44. data/spec/acts_as_approvable/ownership/class_methods_spec.rb +101 -0
  45. data/spec/acts_as_approvable/ownership/instance_methods_spec.rb +32 -0
  46. data/spec/acts_as_approvable/ownership_spec.rb +51 -0
  47. data/spec/acts_as_approvable_spec.rb +29 -0
  48. data/spec/spec_helper.rb +51 -0
  49. data/spec/support/database.rb +46 -0
  50. data/spec/support/database.yml +12 -0
  51. data/spec/support/matchers.rb +87 -0
  52. data/spec/support/models.rb +60 -0
  53. data/{test → spec/support}/schema.rb +15 -9
  54. metadata +156 -53
  55. data/gemfiles/rails32.gemfile +0 -8
  56. data/gemfiles/rails32.gemfile.lock +0 -99
  57. data/lib/acts_as_approvable/acts_as_approvable.rb +0 -291
  58. data/test/acts_as_approvable_model_test.rb +0 -459
  59. data/test/acts_as_approvable_ownership_test.rb +0 -132
  60. data/test/acts_as_approvable_schema_test.rb +0 -13
  61. data/test/acts_as_approvable_test.rb +0 -8
  62. data/test/database.yml +0 -7
  63. data/test/support.rb +0 -19
  64. data/test/test_helper.rb +0 -62
@@ -0,0 +1,61 @@
1
+ module ActsAsApprovable
2
+ module Model
3
+ ##
4
+ # Instance methods that apply to the :update event specifically.
5
+ module UpdateInstanceMethods
6
+ ##
7
+ # Retrieve all approval records for `:update` events.
8
+ def update_approvals(all = true)
9
+ all ? approvals.find_all_by_event('update') : approvals.find_all_by_event_and_state('update', 0)
10
+ end
11
+
12
+ ##
13
+ # Returns true if the record has any `#update_approvals` that are pending
14
+ # approval.
15
+ def pending_changes?
16
+ !update_approvals(false).empty?
17
+ end
18
+
19
+ ##
20
+ # Returns true if any notable (eg. not ignored) fields have been changed.
21
+ def changed_notably?
22
+ notably_changed.any?
23
+ end
24
+
25
+ ##
26
+ # Returns an array of any notable (eg. not ignored) fields that have not
27
+ # been changed.
28
+ #
29
+ # @return [Array] a list of changed fields.
30
+ def notably_changed
31
+ approvable_fields.select { |field| changed.include?(field) }
32
+ end
33
+
34
+ ##
35
+ # Returns a list of fields that require approval.
36
+ def approvable_fields
37
+ self.class.approvable_fields
38
+ end
39
+
40
+ private
41
+ def approvable_update?
42
+ approvals_enabled? and approvable_on?(:update) and changed_notably?
43
+ end
44
+
45
+ def approvable_update
46
+ changed = {}
47
+ originals = {}
48
+
49
+ notably_changed.each do |attr|
50
+ original, changed_to = changes[attr]
51
+
52
+ write_attribute(attr.to_s, original)
53
+ changed[attr] = changed_to
54
+ originals[attr] = original
55
+ end
56
+
57
+ @approval = approvals.build(:event => 'update', :state => 'pending', :object => changed, :original => originals)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,7 +1,7 @@
1
1
  module ActsAsApprovable
2
2
  class Railtie < Rails::Railtie
3
3
  initializer 'acts_as_approvable.configure_rails_initialization' do |app|
4
- ActiveRecord::Base.send :include, ActsAsApprovable::Model
4
+ ActiveRecord::Base.send :extend, ActsAsApprovable::Model
5
5
  end
6
6
  end
7
7
  end
@@ -1,13 +1,14 @@
1
1
  class CreateApprovals < ActiveRecord::Migration
2
2
  def self.up
3
3
  create_table :approvals do |t|
4
- t.string :item_type, :null => false
5
- t.integer :item_id, :null => false
6
- t.string :event, :null => false
7
- t.integer :state, :null => false, :default => 0
8
- <% if options[:owner] %> t.integer :owner_id
9
- <% end %> t.text :object
10
- t.text :reason
4
+ t.string :item_type, :null => false
5
+ t.integer :item_id, :null => false
6
+ t.string :event, :null => false
7
+ t.integer :state, :null => false, :default => 0
8
+ <% if options[:owner] %> t.integer :owner_id
9
+ <% end %> t.text :object, :limit => 16777216
10
+ t.text :original, :limit => 16777216
11
+ t.text :reason
11
12
 
12
13
  t.timestamps
13
14
  end
@@ -0,0 +1,475 @@
1
+ require 'spec_helper'
2
+
3
+ describe Approval do
4
+ before(:each) do
5
+ subject.stub(:save! => true, :save => true)
6
+ end
7
+
8
+ it 'should serialize :object' do
9
+ described_class.serialized_attributes.keys.should include('object')
10
+ end
11
+
12
+ describe '.associations' do
13
+ it { should belong_to(:item) }
14
+ end
15
+
16
+ describe '.validates' do
17
+ it { should validate_presence_of(:item) }
18
+ it { should validate_inclusion_of(:event).in(%w(create update)) }
19
+ it { should validate_numericality_of(:state) }
20
+ it { should ensure_inclusion_of(:state).in_range(0..(described_class::STATES.length - 1)).with_low_message(/greater than/).with_high_message(/less than/)}
21
+ end
22
+
23
+ describe '.enumerate_state' do
24
+ it 'enumerates "pending" to 0' do
25
+ described_class.enumerate_state('pending').should be(0)
26
+ end
27
+
28
+ it 'enumerates "approved" to 1' do
29
+ described_class.enumerate_state('approved').should be(1)
30
+ end
31
+
32
+ it 'enumerates "rejected" to 2' do
33
+ described_class.enumerate_state('rejected').should be(2)
34
+ end
35
+
36
+ it 'enumerates other values to nil' do
37
+ described_class.enumerate_state('not_a_state').should_not be
38
+ end
39
+ end
40
+
41
+ describe '.enumerate_states' do
42
+ it 'enumerates many states at once' do
43
+ described_class.enumerate_states('pending', 'approved', 'rejected').should == [0, 1, 2]
44
+ end
45
+
46
+ it 'ignores states it does not know' do
47
+ described_class.enumerate_states('pending', 'not_a_state', 'rejected').should == [0, 2]
48
+ end
49
+ end
50
+
51
+ describe '.options_for_state' do
52
+ it 'returns an array usable by #options_for_select' do
53
+ described_class.options_for_state.should be_an_options_array
54
+ end
55
+
56
+ it 'includes an "all" option' do
57
+ described_class.options_for_state.should include(['All', -1])
58
+ end
59
+
60
+ it 'includes the pending state' do
61
+ described_class.options_for_state.should include(['Pending', 0])
62
+ end
63
+
64
+ it 'includes the approved state' do
65
+ described_class.options_for_state.should include(['Approved', 1])
66
+ end
67
+
68
+ it 'includes the rejected state' do
69
+ described_class.options_for_state.should include(['Rejected', 2])
70
+ end
71
+ end
72
+
73
+ describe '.options_for_type' do
74
+ before(:each) do
75
+ @default = DefaultApprovable.create
76
+ @creates = CreatesApprovable.create
77
+ @updates = UpdatesApprovable.create
78
+ end
79
+
80
+ it 'returns an array usable by #options_for_select' do
81
+ described_class.options_for_type.should be_an_options_array
82
+ end
83
+
84
+ it 'includes all types with approvals' do
85
+ described_class.options_for_type.should include('DefaultApprovable')
86
+ described_class.options_for_type.should include('CreatesApprovable')
87
+ end
88
+
89
+ it 'does not include types without approvals' do
90
+ described_class.options_for_type.should_not include('UpdatesApprovable')
91
+ end
92
+
93
+ it 'includes a prompt if requested' do
94
+ described_class.options_for_type(true).should include(['All Types', nil])
95
+ end
96
+
97
+ it 'does not includes a prompt by default' do
98
+ described_class.options_for_type.should_not include(['All Types', nil])
99
+ end
100
+ end
101
+
102
+ describe '#state' do
103
+ it 'returns the state as a string' do
104
+ subject.state.should be_a(String)
105
+ end
106
+
107
+ it 'attempts to read the state attribute' do
108
+ subject.should_receive(:read_attribute).with(:state)
109
+ subject.state
110
+ end
111
+ end
112
+
113
+ describe '#state_was' do
114
+ it 'returns the state as a string' do
115
+ subject.state_was.should be_a(String)
116
+ end
117
+
118
+ it 'attempts to read the changed state attribute' do
119
+ subject.should_receive(:changed_attributes).and_return({:state => 1})
120
+ subject.state_was.should == 'approved'
121
+ end
122
+ end
123
+
124
+ describe '#state=' do
125
+ it 'writes the attribute value' do
126
+ subject.should_receive(:write_attribute)
127
+ subject.send(:state=, 'pending')
128
+ end
129
+
130
+ it 'enumerates the given state' do
131
+ subject.should_receive(:write_attribute).with(:state, 0)
132
+ subject.send(:state=, 'pending')
133
+ end
134
+
135
+ it 'skips enumeration for numeric values' do
136
+ subject.should_receive(:write_attribute).with(:state, 10)
137
+ subject.send(:state=, 10)
138
+ end
139
+ end
140
+
141
+ context 'when the state is pending' do
142
+ before(:each) do
143
+ subject.stub(:state => 'pending')
144
+ end
145
+
146
+ it { should be_pending }
147
+ it { should_not be_approved }
148
+ it { should_not be_rejected }
149
+ it { should_not be_locked }
150
+ it { should be_unlocked }
151
+ end
152
+
153
+ context 'when the state is approved' do
154
+ before(:each) do
155
+ subject.stub(:state => 'approved')
156
+ end
157
+
158
+ it { should_not be_pending }
159
+ it { should be_approved }
160
+ it { should_not be_rejected }
161
+ it { should be_locked }
162
+ it { should_not be_unlocked }
163
+ end
164
+
165
+ context 'when the state is rejected' do
166
+ before(:each) do
167
+ subject.stub(:state => 'rejected')
168
+ end
169
+
170
+ it { should_not be_pending }
171
+ it { should_not be_approved }
172
+ it { should be_rejected }
173
+ it { should be_locked }
174
+ it { should_not be_unlocked }
175
+ end
176
+
177
+ context 'when the event is :update' do
178
+ before(:each) do
179
+ subject.stub(:event => 'update')
180
+ end
181
+
182
+ it { should be_update }
183
+ it { should_not be_create }
184
+ end
185
+
186
+ context 'when the event is :create' do
187
+ before(:each) do
188
+ subject.stub(:event => 'create')
189
+ end
190
+
191
+ it { should_not be_update }
192
+ it { should be_create }
193
+ end
194
+
195
+ context 'when the approval is unlocked' do
196
+ before(:each) do
197
+ @item = DefaultApprovable.without_approval { |m| m.create }
198
+ subject.stub(:locked? => false, :created_at => Time.now, :item => @item)
199
+ @item.stub(:updated_at => Time.now)
200
+ end
201
+
202
+ describe '#able_to_save?' do
203
+ it { should be_able_to_save }
204
+
205
+ it 'does not check the what the state was' do
206
+ subject.should_not_receive(:state_was)
207
+ subject.able_to_save?
208
+ end
209
+ end
210
+
211
+ describe '#stale?' do
212
+ it 'checks when the item was changed' do
213
+ @item.should_receive(:has_attribute?).with(:updated_at).and_return(true)
214
+ subject.stale?
215
+ end
216
+ end
217
+
218
+ describe '#fresh?' do
219
+ it 'checks when the item was changed' do
220
+ @item.should_receive(:has_attribute?).with(:updated_at).and_return(true)
221
+ subject.fresh?
222
+ end
223
+ end
224
+
225
+ context 'when the approval is newer than the last update' do
226
+ before(:each) do
227
+ subject.stub(:created_at => @item.updated_at + 60)
228
+ end
229
+
230
+ it { should_not be_stale }
231
+ it { should be_fresh }
232
+ end
233
+
234
+ context 'when the approval is older than the last update' do
235
+ before(:each) do
236
+ subject.stub(:created_at => @item.updated_at - 60)
237
+ end
238
+
239
+ it { should be_stale }
240
+ it { should_not be_fresh }
241
+ end
242
+ end
243
+
244
+ context 'when the approval is locked' do
245
+ before(:each) do
246
+ subject.stub(:locked? => true)
247
+ end
248
+
249
+ it { should_not be_stale }
250
+ it { should be_fresh }
251
+
252
+ describe '#able_to_save?' do
253
+ it 'checks the what the state was' do
254
+ subject.should_receive(:state_was)
255
+ subject.able_to_save?
256
+ end
257
+ end
258
+
259
+ describe '#stale?' do
260
+ it 'does not check when the item was changed' do
261
+ subject.should_not_receive(:item)
262
+ subject.stale?
263
+ end
264
+ end
265
+
266
+ describe '#fresh?' do
267
+ it 'does not check when the item was changed' do
268
+ subject.should_not_receive(:item)
269
+ subject.fresh?
270
+ end
271
+ end
272
+
273
+ describe '#approve!' do
274
+ it 'raises a Locked exception' do
275
+ expect { subject.approve! }.to raise_error(ActsAsApprovable::Error::Locked)
276
+ end
277
+
278
+ it 'leaves the approval in a pending state' do
279
+ begin; subject.approve!; rescue ActsAsApprovable::Error::Locked; end
280
+ subject.should be_pending
281
+ end
282
+ end
283
+
284
+ describe '#reject!' do
285
+ it 'raises a Locked exception' do
286
+ expect { subject.reject! }.to raise_error(ActsAsApprovable::Error::Locked)
287
+ end
288
+
289
+ it 'leaves the approval in a pending state' do
290
+ begin; subject.reject!; rescue ActsAsApprovable::Error::Locked; end
291
+ subject.should be_pending
292
+ end
293
+ end
294
+
295
+ context 'and the state is pending' do
296
+ before(:each) do
297
+ subject.stub(:state_was => 'pending')
298
+ end
299
+
300
+ it { should be_able_to_save }
301
+ end
302
+
303
+ context 'and the state is approved' do
304
+ before(:each) do
305
+ subject.stub(:state_was => 'approved')
306
+ end
307
+
308
+ it { should_not be_able_to_save }
309
+ end
310
+
311
+ context 'and the state is rejected' do
312
+ before(:each) do
313
+ subject.stub(:state_was => 'rejected')
314
+ end
315
+
316
+ it { should_not be_able_to_save }
317
+ end
318
+ end
319
+
320
+ context 'when the approval is stale' do
321
+ before(:each) do
322
+ @item = DefaultApprovable.without_approval { |m| m.create }
323
+ subject.stub(:stale? => true, :item => @item)
324
+ end
325
+
326
+ it { should be_stale }
327
+ it { should_not be_fresh }
328
+
329
+ describe '#approve!' do
330
+ it 'raises a Stale exception' do
331
+ expect { subject.approve! }.to raise_error(ActsAsApprovable::Error::Stale)
332
+ end
333
+
334
+ it 'leaves the approval in a pending state' do
335
+ begin; subject.approve!; rescue ActsAsApprovable::Error::Stale; end
336
+ subject.should be_pending
337
+ end
338
+
339
+ context 'when approval is forced' do
340
+ it 'does not raise a Stale exception' do
341
+ expect { subject.approve!(true) }.to_not raise_error(ActsAsApprovable::Error::Stale)
342
+ end
343
+
344
+ it 'leaves the approval in a pending state' do
345
+ subject.approve!(true)
346
+ subject.should be_approved
347
+ end
348
+ end
349
+ end
350
+
351
+ describe '#reject!' do
352
+ it 'does not raise a Stale exception' do
353
+ expect { subject.reject! }.to_not raise_error(ActsAsApprovable::Error::Stale)
354
+ end
355
+
356
+ it 'moves the approval to a rejected state' do
357
+ subject.reject!
358
+ subject.should be_rejected
359
+ end
360
+ end
361
+ end
362
+
363
+ context 'when the approval is unlocked and fresh' do
364
+ before(:each) do
365
+ @item = DefaultApprovable.without_approval { |m| m.create }
366
+ subject.stub(:locked? => false, :stale? => false, :item => @item, :object => {})
367
+ end
368
+
369
+ it { should_not be_stale }
370
+ it { should be_fresh }
371
+
372
+ describe '#approve!' do
373
+ it 'does not raise an exception' do
374
+ expect { subject.approve! }.to_not raise_error
375
+ end
376
+
377
+ it 'moves the approval to an approved state' do
378
+ subject.approve!
379
+ subject.should be_approved
380
+ end
381
+
382
+ it 'calls the before and after callbacks' do
383
+ subject.should_receive(:run_item_callback).with(:before_approve).once.and_return(true)
384
+ subject.should_receive(:run_item_callback).with(:after_approve).once
385
+ subject.approve!
386
+ end
387
+
388
+ context 'when the event is :update' do
389
+ before(:each) do
390
+ subject.stub(:event => 'update')
391
+ end
392
+
393
+ it 'sets the item attributes' do
394
+ @item.should_receive(:attributes=)
395
+ subject.approve!
396
+ end
397
+
398
+ it 'does not set the local item state' do
399
+ @item.should_not_receive(:set_approval_state)
400
+ subject.approve!
401
+ end
402
+ end
403
+
404
+ context 'when the event is :create' do
405
+ before(:each) do
406
+ subject.stub(:event => 'create')
407
+ end
408
+
409
+ it 'does not set the item attributes' do
410
+ @item.should_not_receive(:attributes=)
411
+ subject.approve!
412
+ end
413
+
414
+ it 'sets the local item state' do
415
+ @item.should_receive(:set_approval_state).with('approved')
416
+ subject.approve!
417
+ end
418
+ end
419
+ end
420
+
421
+ describe '#reject!' do
422
+ it 'does not raise an exception' do
423
+ expect { subject.reject! }.to_not raise_error
424
+ end
425
+
426
+ it 'moves the approval to a rejected state' do
427
+ subject.reject!
428
+ subject.should be_rejected
429
+ end
430
+
431
+ it 'sets the reason if given' do
432
+ subject.reject!('reason')
433
+ subject.reason.should == 'reason'
434
+ end
435
+
436
+ it 'calls the before and after callbacks' do
437
+ subject.should_receive(:run_item_callback).with(:before_reject).once.and_return(true)
438
+ subject.should_receive(:run_item_callback).with(:after_reject).once
439
+ subject.reject!
440
+ end
441
+
442
+ context 'when the event is :update' do
443
+ before(:each) do
444
+ subject.stub(:event => 'update')
445
+ end
446
+
447
+ it 'does not set the item attributes' do
448
+ @item.should_not_receive(:attributes=)
449
+ subject.reject!
450
+ end
451
+
452
+ it 'does not set the local item state' do
453
+ @item.should_not_receive(:set_approval_state)
454
+ subject.reject!
455
+ end
456
+ end
457
+
458
+ context 'when the event is :create' do
459
+ before(:each) do
460
+ subject.stub(:event => 'create')
461
+ end
462
+
463
+ it 'does not set the item attributes' do
464
+ @item.should_not_receive(:attributes=)
465
+ subject.reject!
466
+ end
467
+
468
+ it 'sets the local item state' do
469
+ @item.should_receive(:set_approval_state).with('rejected')
470
+ subject.reject!
471
+ end
472
+ end
473
+ end
474
+ end
475
+ end