workflow_on_mongoid 0.8.0.3 → 0.8.0.4

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.
@@ -0,0 +1,487 @@
1
+ require 'test_helper'
2
+
3
+ require 'mongoid'
4
+ require 'workflow'
5
+ require 'workflow_on_mongoid'
6
+
7
+ # redefine this so it works with mongoid!
8
+ def assert_state(title, expected_state, klass = Order)
9
+ puts 'mongoid assert_state'
10
+ o = klass.where(:title => title).first
11
+ assert_equal expected_state, o.send(klass.workflow_column)
12
+ o
13
+ end
14
+
15
+
16
+ class MongoidOrder
17
+ include Mongoid::Document
18
+
19
+ field :title
20
+
21
+ include Workflow
22
+ workflow do
23
+ state :submitted do
24
+ event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
25
+ end
26
+ end
27
+ state :accepted do
28
+ event :ship, :transitions_to => :shipped
29
+ end
30
+ state :shipped
31
+ end
32
+ end
33
+
34
+ class MongoidLegacyOrder
35
+ include Mongoid::Document
36
+
37
+ field :title
38
+ field :foo_bar
39
+
40
+ include Workflow
41
+
42
+ workflow_column :foo_bar # use this legacy database column for persistence
43
+
44
+ workflow do
45
+ state :submitted do
46
+ event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
47
+ end
48
+ end
49
+ state :accepted do
50
+ event :ship, :transitions_to => :shipped
51
+ end
52
+ state :shipped
53
+ end
54
+ end
55
+
56
+ class MongoidImage
57
+ include Mongoid::Document
58
+
59
+ field :title
60
+ field :status
61
+
62
+ include Workflow
63
+
64
+ workflow_column :status
65
+
66
+ workflow do
67
+ state :unconverted do
68
+ event :convert, :transitions_to => :converted
69
+ end
70
+ state :converted
71
+ end
72
+ end
73
+
74
+ class MongoidSmallImage < MongoidImage
75
+ end
76
+
77
+ class MongoidSpecialSmallImage < MongoidSmallImage
78
+ end
79
+
80
+ class MongoidTest < MongoidTestCase
81
+
82
+ def setup
83
+ super
84
+ MongoidOrder.create!(:title => 'some MongoidOrder', :workflow_state => 'accepted')
85
+ MongoidLegacyOrder.create!(:title => 'some MongoidOrder', :foo_bar => 'accepted')
86
+ end
87
+
88
+ def assert_state(title, expected_state, klass = MongoidOrder)
89
+ o = klass.where(:title => title).first
90
+ assert_equal expected_state, o.send(klass.workflow_column)
91
+ o
92
+ end
93
+
94
+ test 'immediately save the new workflow_state on state machine transition' do
95
+ o = assert_state 'some MongoidOrder', 'accepted'
96
+ assert o.ship!
97
+ assert_state 'some MongoidOrder', 'shipped'
98
+ end
99
+
100
+ test 'immediately save the new workflow_state on state machine transition with custom column name' do
101
+ o = assert_state 'some MongoidOrder', 'accepted', MongoidLegacyOrder
102
+ assert o.ship!
103
+ assert_state 'some MongoidOrder', 'shipped', MongoidLegacyOrder
104
+ end
105
+
106
+ # There was a bug where calling valid? on the record would cause it to be saved
107
+ # see https://github.com/bowsersenior/workflow_on_mongoid/pull/3
108
+ test 'does not save the record when setting initial state' do
109
+ new_order = MongoidOrder.new
110
+ assert new_order.new_record?
111
+
112
+ new_order.valid?
113
+ assert new_order.new_record?
114
+ end
115
+
116
+ test 'persist workflow_state in the db and reload' do
117
+ o = assert_state 'some MongoidOrder', 'accepted'
118
+ assert_equal :accepted, o.current_state.name
119
+ o.ship!
120
+ o.save!
121
+
122
+ assert_state 'some MongoidOrder', 'shipped'
123
+
124
+ o.reload
125
+ assert_equal 'shipped', o.send(:workflow_state)
126
+ end
127
+
128
+ test 'persist workflow_state in the db with_custom_name and reload' do
129
+ o = assert_state 'some MongoidOrder', 'accepted', MongoidLegacyOrder
130
+ assert_equal :accepted, o.current_state.name
131
+ o.ship!
132
+ o.save!
133
+
134
+ assert_state 'some MongoidOrder', 'shipped', MongoidLegacyOrder
135
+
136
+ o.reload
137
+ assert_equal 'shipped', o.send(:foo_bar)
138
+ end
139
+
140
+ test 'default workflow column should be workflow_state' do
141
+ o = assert_state 'some MongoidOrder', 'accepted'
142
+ assert_equal :workflow_state, o.class.workflow_column
143
+ end
144
+
145
+ test 'custom workflow column should be foo_bar' do
146
+ o = assert_state 'some MongoidOrder', 'accepted', MongoidLegacyOrder
147
+ assert_equal :foo_bar, o.class.workflow_column
148
+ end
149
+
150
+ test 'access workflow specification' do
151
+ assert_equal 3, MongoidOrder.workflow_spec.states.length
152
+ assert_equal ['submitted', 'accepted', 'shipped'].sort,
153
+ MongoidOrder.workflow_spec.state_names.map{|n| n.to_s}.sort
154
+ end
155
+
156
+ test 'current state object' do
157
+ o = assert_state 'some MongoidOrder', 'accepted'
158
+ assert_equal 'accepted', o.current_state.to_s
159
+ assert_equal 1, o.current_state.events.length
160
+ end
161
+
162
+ test 'on_entry and on_exit invoked' do
163
+ c = Class.new
164
+ callbacks = mock()
165
+ callbacks.expects(:my_on_exit_new).once
166
+ callbacks.expects(:my_on_entry_old).once
167
+ c.class_eval do
168
+ include Workflow
169
+ workflow do
170
+ state :new do
171
+ event :age, :transitions_to => :old
172
+ end
173
+ on_exit do
174
+ callbacks.my_on_exit_new
175
+ end
176
+ state :old
177
+ on_entry do
178
+ callbacks.my_on_entry_old
179
+ end
180
+ on_exit do
181
+ fail "wrong on_exit executed"
182
+ end
183
+ end
184
+ end
185
+
186
+ o = c.new
187
+ assert_equal 'new', o.current_state.to_s
188
+ o.age!
189
+ end
190
+
191
+ test 'on_transition invoked' do
192
+ callbacks = mock()
193
+ callbacks.expects(:on_tran).once # this is validated at the end
194
+ c = Class.new
195
+ c.class_eval do
196
+ include Workflow
197
+ workflow do
198
+ state :one do
199
+ event :increment, :transitions_to => :two
200
+ end
201
+ state :two
202
+ on_transition do |from, to, triggering_event, *event_args|
203
+ callbacks.on_tran
204
+ end
205
+ end
206
+ end
207
+ assert_not_nil c.workflow_spec.on_transition_proc
208
+ c.new.increment!
209
+ end
210
+
211
+ test 'access event meta information' do
212
+ c = Class.new
213
+ c.class_eval do
214
+ include Workflow
215
+ workflow do
216
+ state :main, :meta => {:importance => 8}
217
+ state :supplemental, :meta => {:importance => 1}
218
+ end
219
+ end
220
+ assert_equal 1, c.workflow_spec.states[:supplemental].meta[:importance]
221
+ end
222
+
223
+ test 'initial state' do
224
+ c = Class.new
225
+ c.class_eval do
226
+ include Workflow
227
+ workflow { state :one; state :two }
228
+ end
229
+ assert_equal 'one', c.new.current_state.to_s
230
+ end
231
+
232
+ test 'nil as initial state' do
233
+ MongoidOrder.create!(:title => 'nil state', :workflow_state => nil)
234
+ o = MongoidOrder.where(:title =>'nil state').first
235
+ assert o.submitted?, 'if workflow_state is nil, the initial state should be assumed'
236
+ assert !o.shipped?
237
+ end
238
+
239
+ test 'initial state immediately set for new objects' do
240
+ o = MongoidOrder.create(:title => 'new object')
241
+ assert_equal 'submitted', o.send(:workflow_state)
242
+ end
243
+
244
+ test 'question methods for state' do
245
+ o = assert_state 'some MongoidOrder', 'accepted'
246
+ assert o.accepted?
247
+ assert !o.shipped?
248
+ end
249
+
250
+ test 'correct exception for event that is not allowed in current state' do
251
+ o = assert_state 'some MongoidOrder', 'accepted'
252
+
253
+ assert_raise Workflow::NoTransitionAllowed do
254
+ o.accept!
255
+ end
256
+ end
257
+
258
+ test 'multiple events with the same name and different arguments lists from different states'
259
+
260
+ test 'implicit transition callback' do
261
+ args = mock()
262
+ args.expects(:my_tran).once # this is validated at the end
263
+ c = Class.new
264
+ c.class_eval do
265
+ include Workflow
266
+ def my_transition(args)
267
+ args.my_tran
268
+ end
269
+ workflow do
270
+ state :one do
271
+ event :my_transition, :transitions_to => :two
272
+ end
273
+ state :two
274
+ end
275
+ end
276
+ c.new.my_transition!(args)
277
+ end
278
+
279
+ test 'Single table inheritance (STI)' do
280
+ class BigMongoidOrder < MongoidOrder
281
+ end
282
+
283
+ bo = BigMongoidOrder.new
284
+ assert bo.submitted?
285
+ assert !bo.accepted?
286
+ end
287
+
288
+ test 'STI when parent changed the workflow_state column' do
289
+ assert_equal 'status', MongoidImage.workflow_column.to_s
290
+ assert_equal 'status', MongoidSmallImage.workflow_column.to_s
291
+ assert_equal 'status', MongoidSpecialSmallImage.workflow_column.to_s
292
+ end
293
+
294
+ test 'Two-level inheritance' do
295
+ class BigMongoidOrder < MongoidOrder
296
+ end
297
+
298
+ class EvenBiggerMongoidOrder < BigMongoidOrder
299
+ end
300
+
301
+ assert EvenBiggerMongoidOrder.new.submitted?
302
+ end
303
+
304
+ test 'Iheritance with workflow definition override' do
305
+ class BigMongoidOrder < MongoidOrder
306
+ end
307
+
308
+ class SpecialBigMongoidOrder < BigMongoidOrder
309
+ workflow do
310
+ state :start_big
311
+ end
312
+ end
313
+
314
+ special = SpecialBigMongoidOrder.new
315
+ assert_equal 'start_big', special.current_state.to_s
316
+ end
317
+
318
+ test 'Better error message for missing target state' do
319
+ class Problem
320
+ include Workflow
321
+ workflow do
322
+ state :initial do
323
+ event :solve, :transitions_to => :solved
324
+ end
325
+ end
326
+ end
327
+ assert_raise Workflow::WorkflowError do
328
+ Problem.new.solve!
329
+ end
330
+ end
331
+
332
+ # Intermixing of transition graph definition (states, transitions)
333
+ # on the one side and implementation of the actions on the other side
334
+ # for a bigger state machine can introduce clutter.
335
+ #
336
+ # To reduce this clutter it is now possible to use state entry- and
337
+ # exit- hooks defined through a naming convention. For example, if there
338
+ # is a state :pending, then you can hook in by defining method
339
+ # `def on_pending_exit(new_state, event, *args)` instead of using a
340
+ # block:
341
+ #
342
+ # state :pending do
343
+ # on_entry do
344
+ # # your implementation here
345
+ # end
346
+ # end
347
+ #
348
+ # If both a function with a name according to naming convention and the
349
+ # on_entry/on_exit block are given, then only on_entry/on_exit block is used.
350
+ test 'on_entry and on_exit hooks in separate methods' do
351
+ c = Class.new
352
+ c.class_eval do
353
+ include Workflow
354
+ attr_reader :history
355
+ def initialize
356
+ @history = []
357
+ end
358
+ workflow do
359
+ state :new do
360
+ event :next, :transitions_to => :next_state
361
+ end
362
+ state :next_state
363
+ end
364
+
365
+ def on_next_state_entry(prior_state, event, *args)
366
+ @history << "on_next_state_entry #{event} #{prior_state} ->"
367
+ end
368
+
369
+ def on_new_exit(new_state, event, *args)
370
+ @history << "on_new_exit #{event} -> #{new_state}"
371
+ end
372
+ end
373
+
374
+ o = c.new
375
+ assert_equal 'new', o.current_state.to_s
376
+ assert_equal [], o.history
377
+ o.next!
378
+ assert_equal ['on_new_exit next -> next_state', 'on_next_state_entry next new ->'], o.history
379
+
380
+ end
381
+
382
+ test 'diagram generation' do
383
+ begin
384
+ $stdout = StringIO.new('', 'w')
385
+ Workflow::create_workflow_diagram(MongoidOrder, 'doc')
386
+ assert_match(/open.+\.pdf/, $stdout.string,
387
+ 'PDF should be generate and a hint be given to the user.')
388
+ ensure
389
+ $stdout = STDOUT
390
+ end
391
+ end
392
+
393
+ test 'halt stops the transition' do
394
+ c = Class.new do
395
+ include Workflow
396
+ workflow do
397
+ state :young do
398
+ event :age, :transitions_to => :old
399
+ end
400
+ state :old
401
+ end
402
+
403
+ def age(by=1)
404
+ halt 'too fast' if by > 100
405
+ end
406
+ end
407
+
408
+ joe = c.new
409
+ assert joe.young?
410
+ joe.age! 120
411
+ assert joe.young?, 'Transition should have been halted'
412
+ assert_equal 'too fast', joe.halted_because
413
+ end
414
+
415
+ test 'halt! raises exception immediately' do
416
+ article_class = Class.new do
417
+ include Workflow
418
+ attr_accessor :too_far
419
+ workflow do
420
+ state :new do
421
+ event :reject, :transitions_to => :rejected
422
+ end
423
+ state :rejected
424
+ end
425
+
426
+ def reject(reason)
427
+ halt! 'We do not reject articles unless the reason is important' \
428
+ unless reason =~ /important/i
429
+ self.too_far = "This line should not be executed"
430
+ end
431
+ end
432
+
433
+ article = article_class.new
434
+ assert article.new?
435
+ assert_raise Workflow::TransitionHalted do
436
+ article.reject! 'Too funny'
437
+ end
438
+ assert_nil article.too_far
439
+ assert article.new?, 'Transition should have been halted'
440
+ article.reject! 'Important: too short'
441
+ assert article.rejected?, 'Transition should happen now'
442
+ end
443
+
444
+ # published gem doesn't have the `can_?` methods yet!
445
+ # test 'can fire event?' do
446
+ # c = Class.new do
447
+ # include Workflow
448
+ # workflow do
449
+ # state :newborn do
450
+ # event :go_to_school, :transitions_to => :schoolboy
451
+ # end
452
+ # state :schoolboy do
453
+ # event :go_to_college, :transitions_to => :student
454
+ # end
455
+ # state :student
456
+ # end
457
+ # end
458
+ #
459
+ # human = c.new
460
+ # assert human.can_go_to_school?
461
+ # assert_equal false, human.can_go_to_college?
462
+ # end
463
+
464
+ test 'workflow graph generation' do
465
+ Dir.chdir('tmp') do
466
+ capture_streams do
467
+ Workflow::create_workflow_diagram(MongoidOrder)
468
+ end
469
+ end
470
+ end
471
+
472
+ test 'workflow graph generation in path with spaces' do
473
+ `mkdir -p '/tmp/Workflow test'`
474
+ capture_streams do
475
+ Workflow::create_workflow_diagram(MongoidOrder, '/tmp/Workflow test')
476
+ end
477
+ end
478
+
479
+ def capture_streams
480
+ old_stdout = $stdout
481
+ $stdout = captured_stdout = StringIO.new
482
+ yield
483
+ $stdout = old_stdout
484
+ captured_stdout
485
+ end
486
+
487
+ end
@@ -0,0 +1,80 @@
1
+ require 'test_helper'
2
+
3
+ unless RUBY_VERSION =~ /^1\.9/
4
+ class MultipleWorkflowsTest < ActiveRecordTestCase
5
+
6
+ test 'multiple workflows' do
7
+
8
+ ActiveRecord::Schema.define do
9
+ create_table :bookings do |t|
10
+ t.string :title, :null => false
11
+ t.string :workflow_state
12
+ t.string :workflow_type
13
+ end
14
+ end
15
+
16
+ exec "INSERT INTO bookings(title, workflow_state, workflow_type) VALUES('booking1', 'initial', 'workflow_1')"
17
+ exec "INSERT INTO bookings(title, workflow_state, workflow_type) VALUES('booking2', 'initial', 'workflow_2')"
18
+
19
+ class Booking < ActiveRecord::Base
20
+ def initialize_workflow
21
+ # define workflow per object instead of per class
22
+ case workflow_type
23
+ when 'workflow_1'
24
+ class << self
25
+ include Workflow
26
+ workflow do
27
+ state :initial do
28
+ event :progress, :transitions_to => :last
29
+ end
30
+ state :last
31
+ end
32
+ end
33
+ when 'workflow_2'
34
+ class << self
35
+ include Workflow
36
+ workflow do
37
+ state :initial do
38
+ event :progress, :transitions_to => :intermediate
39
+ end
40
+ state :intermediate
41
+ state :last
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def metaclass; class << self; self; end; end
48
+
49
+ def workflow_spec
50
+ metaclass.workflow_spec
51
+ end
52
+
53
+ end
54
+
55
+ booking1 = Booking.find_by_title('booking1')
56
+ booking1.initialize_workflow
57
+
58
+ booking2 = Booking.find_by_title('booking2')
59
+ booking2.initialize_workflow
60
+
61
+ assert booking1.initial?
62
+ booking1.progress!
63
+ assert booking1.last?, 'booking1 should transition to the "last" state'
64
+
65
+ assert booking2.initial?
66
+ booking2.progress!
67
+ assert booking2.intermediate?, 'booking2 should transition to the "intermediate" state'
68
+
69
+ assert booking1.workflow_spec, 'can access the individual workflow specification'
70
+ assert_equal 2, booking1.workflow_spec.states.length
71
+ assert_equal 3, booking2.workflow_spec.states.length
72
+ end
73
+
74
+ class Object
75
+ # The hidden singleton lurks behind everyone
76
+ def metaclass; class << self; self; end; end
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,37 @@
1
+ require 'workflow'
2
+ class Article
3
+ include Workflow
4
+ workflow do
5
+ state :new do
6
+ event :submit, :transitions_to => :awaiting_review
7
+ end
8
+ state :awaiting_review do
9
+ event :review, :transitions_to => :being_reviewed
10
+ end
11
+ state :being_reviewed do
12
+ event :accept, :transitions_to => :accepted
13
+ event :reject, :transitions_to => :rejected
14
+ end
15
+ state :accepted
16
+ state :rejected
17
+ end
18
+ end
19
+
20
+ article = Article.new
21
+ article.accepted? # => false
22
+ article.new? # => true
23
+ article.submit!
24
+ article.review!
25
+
26
+ puts article.current_state # => being_reviewed
27
+
28
+
29
+ class Article
30
+ def reject
31
+ puts "send email to the author here explaining the reason for the rejection"
32
+ end
33
+ end
34
+
35
+ article.reject! # will cause a state transition, would persist the new
36
+ # state (if inherited from ActiveRecord), and invoke the callback -
37
+ # send email to the author.
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+
5
+ require 'test/unit'
6
+ require 'active_record'
7
+ require 'workflow_on_mongoid'
8
+
9
+ class << Test::Unit::TestCase
10
+ def test(name, &block)
11
+ test_name = :"test_#{name.gsub(' ','_')}"
12
+ raise ArgumentError, "#{test_name} is already defined" if self.instance_methods.include? test_name.to_s
13
+ if block
14
+ define_method test_name, &block
15
+ else
16
+ puts "PENDING: #{name}"
17
+ end
18
+ end
19
+ end
20
+
21
+ class ActiveRecordTestCase < Test::Unit::TestCase
22
+ def exec(sql)
23
+ ActiveRecord::Base.connection.execute sql
24
+ end
25
+
26
+ def setup
27
+ ActiveRecord::Base.establish_connection(
28
+ :adapter => "sqlite3",
29
+ :database => ":memory:" #"tmp/test"
30
+ )
31
+
32
+ # eliminate ActiveRecord warning. TODO: delete as soon as ActiveRecord is fixed
33
+ ActiveRecord::Base.connection.reconnect!
34
+ end
35
+
36
+ def teardown
37
+ ActiveRecord::Base.connection.disconnect!
38
+ end
39
+
40
+ def default_test
41
+ end
42
+ end
43
+
44
+ require 'mongoid'
45
+ class MongoidTestCase < Test::Unit::TestCase
46
+ Mongoid.configure do |config|
47
+ config.master = Mongo::Connection.new('127.0.0.1', 27017).db("workflow_on_mongoid")
48
+ end
49
+
50
+ def teardown
51
+ Mongoid.master.collections.select do |collection|
52
+ collection.name !~ /system/
53
+ end.each(&:drop)
54
+ end
55
+
56
+ def default_test; end
57
+ end
@@ -0,0 +1,57 @@
1
+ # require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ require 'test_helper'
4
+
5
+ require 'workflow'
6
+
7
+ class WithoutWorkflowTest < Test::Unit::TestCase
8
+ class Article
9
+ include Workflow
10
+ workflow do
11
+ state :new do
12
+ event :submit, :transitions_to => :awaiting_review
13
+ end
14
+ state :awaiting_review do
15
+ event :review, :transitions_to => :being_reviewed
16
+ end
17
+ state :being_reviewed do
18
+ event :accept, :transitions_to => :accepted
19
+ event :reject, :transitions_to => :rejected
20
+ end
21
+ state :accepted
22
+ state :rejected
23
+ end
24
+ end
25
+
26
+ def test_readme_example_article
27
+ article = Article.new
28
+ assert article.new?
29
+ end
30
+
31
+ test 'better error message on transitions_to typo' do
32
+ assert_raise Workflow::WorkflowDefinitionError do
33
+ Class.new do
34
+ include Workflow
35
+ workflow do
36
+ state :new do
37
+ event :event1, :transitionnn => :next # missing transitions_to target
38
+ end
39
+ state :next
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ test 'check transition_to alias' do
46
+ Class.new do
47
+ include Workflow
48
+ workflow do
49
+ state :new do
50
+ event :event1, :transition_to => :next
51
+ end
52
+ state :next
53
+ end
54
+ end
55
+ end
56
+ end
57
+