workflow-orchestrator 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.travis.yml +36 -0
  4. data/CHANGELOG.md +133 -0
  5. data/Gemfile +3 -0
  6. data/MIT-LICENSE +22 -0
  7. data/README.md +707 -0
  8. data/Rakefile +30 -0
  9. data/gemfiles/Gemfile.rails-3.x +12 -0
  10. data/gemfiles/Gemfile.rails-4.0 +14 -0
  11. data/gemfiles/Gemfile.rails-4.1 +14 -0
  12. data/gemfiles/Gemfile.rails-4.2 +14 -0
  13. data/gemfiles/Gemfile.rails-edge +14 -0
  14. data/lib/workflow/adapters/active_record.rb +75 -0
  15. data/lib/workflow/adapters/remodel.rb +15 -0
  16. data/lib/workflow/draw.rb +79 -0
  17. data/lib/workflow/errors.rb +20 -0
  18. data/lib/workflow/event.rb +38 -0
  19. data/lib/workflow/event_collection.rb +36 -0
  20. data/lib/workflow/specification.rb +83 -0
  21. data/lib/workflow/state.rb +44 -0
  22. data/lib/workflow/version.rb +3 -0
  23. data/lib/workflow.rb +307 -0
  24. data/orders_workflow.png +0 -0
  25. data/test/active_record_scopes_test.rb +56 -0
  26. data/test/active_record_scopes_with_values_test.rb +79 -0
  27. data/test/adapter_hook_test.rb +52 -0
  28. data/test/advanced_examples_test.rb +84 -0
  29. data/test/advanced_hooks_and_validation_test.rb +119 -0
  30. data/test/attr_protected_test.rb +107 -0
  31. data/test/before_transition_test.rb +36 -0
  32. data/test/couchtiny_example.rb +46 -0
  33. data/test/enum_values_in_memory_test.rb +23 -0
  34. data/test/enum_values_test.rb +30 -0
  35. data/test/incline_column_test.rb +54 -0
  36. data/test/inheritance_test.rb +56 -0
  37. data/test/main_test.rb +588 -0
  38. data/test/multiple_workflows_test.rb +84 -0
  39. data/test/new_versions/compare_states_test.rb +32 -0
  40. data/test/new_versions/persistence_test.rb +62 -0
  41. data/test/on_error_test.rb +52 -0
  42. data/test/on_unavailable_transition_test.rb +85 -0
  43. data/test/readme_example.rb +37 -0
  44. data/test/test_helper.rb +39 -0
  45. data/test/without_active_record_test.rb +54 -0
  46. data/workflow-orchestrator.gemspec +42 -0
  47. metadata +267 -0
data/lib/workflow.rb ADDED
@@ -0,0 +1,307 @@
1
+ require 'rubygems'
2
+
3
+ require 'workflow/specification'
4
+ require 'workflow/adapters/active_record'
5
+ require 'workflow/adapters/remodel'
6
+
7
+ # See also README.markdown for documentation
8
+ module Workflow
9
+ module ClassMethods
10
+ attr_reader :workflow_spec
11
+
12
+ def workflow_column(column_name=nil)
13
+ #I guess we want to preserve the api???
14
+ @workflow_state_column_name ||= column_name
15
+ if @workflow_state_column_name.nil? && superclass.respond_to?(:workflow_column)
16
+ @workflow_state_column_name = superclass.workflow_column
17
+ end
18
+ @workflow_state_column_name ||= :workflow_state
19
+
20
+ end
21
+
22
+ def workflow(column=nil,&specification)
23
+ column = workflow_column(column)
24
+ assign_workflow Specification.new(Hash.new, &specification)
25
+
26
+ inject_setter_for_state
27
+ end
28
+
29
+ private
30
+
31
+ #allow setting of workflow_state by name, and interpolate the value
32
+ def inject_setter_for_state
33
+ define_method("#{@workflow_state_column_name}=") do |val|
34
+
35
+ matching_state = spec.states.select{|k,v| v.name.to_s == val.to_s}.values.first
36
+ val = matching_state.value if matching_state
37
+ super(val)
38
+ end
39
+ end
40
+
41
+ # Creates the convinience methods like `my_transition!`
42
+ def assign_workflow(specification_object)
43
+
44
+ # Merging two workflow specifications can **not** be done automically, so
45
+ # just make the latest specification win. Same for inheritance -
46
+ # definition in the subclass wins.
47
+ if respond_to? :inherited_workflow_spec # undefine methods defined by the old workflow_spec
48
+ inherited_workflow_spec.states.values.each do |state|
49
+ state_name = state.name
50
+ module_eval do
51
+ undef_method "#{state_name}?"
52
+ end
53
+
54
+ state.events.flat.each do |event|
55
+ event_name = event.name
56
+ module_eval do
57
+ undef_method "#{event_name}!".to_sym
58
+ undef_method "can_#{event_name}?"
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ @workflow_spec = specification_object
65
+ @workflow_spec.states.values.each do |state|
66
+ state_name = state.name
67
+ module_eval do
68
+ define_method "#{state_name}?" do
69
+ state_name == current_state.name
70
+ end
71
+ end
72
+
73
+ state.events.flat.each do |event|
74
+ event_name = event.name
75
+ module_eval do
76
+ define_method "#{event_name}!".to_sym do |*args|
77
+ process_event!(event_name, *args)
78
+ end
79
+
80
+ define_method "can_#{event_name}?" do
81
+ return !!current_state.events.first_applicable(event_name, self)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ module InstanceMethods
90
+
91
+ def current_state
92
+ loaded_state_value = load_workflow_state
93
+ if loaded_state_value
94
+ loaded_state_name = spec.states.select{|k,v| v.value.to_s == loaded_state_value.to_s}.keys.first
95
+ end
96
+
97
+ res = spec.states[loaded_state_name.to_sym] if loaded_state_name
98
+ res || spec.initial_state
99
+ end
100
+
101
+ # See the 'Guards' section in the README
102
+ # @return true if the last transition was halted by one of the transition callbacks.
103
+ def halted?
104
+ @halted
105
+ end
106
+
107
+ # @return the reason of the last transition abort as set by the previous
108
+ # call of `halt` or `halt!` method.
109
+ def halted_because
110
+ @halted_because
111
+ end
112
+
113
+ def process_event!(name, *args)
114
+ event = current_state.events.first_applicable(name, self)
115
+ if event.nil?
116
+ return run_on_unavailable_transition(current_state, name, *args)
117
+ end
118
+ @halted_because = nil
119
+ @halted = false
120
+
121
+ check_transition(event)
122
+
123
+ from = current_state
124
+ to_state = spec.states[event.transitions_to]
125
+ to_value = to_state.value
126
+
127
+ run_before_transition(from, to_state, name, *args)
128
+ return false if @halted
129
+
130
+ begin
131
+ return_value = run_action(event.action, *args) || run_action_callback(event.name, *args)
132
+ rescue StandardError => e
133
+ run_on_error(e, from, to_state, name, *args)
134
+ end
135
+
136
+ return false if @halted
137
+
138
+ run_on_transition(from, to_state, name, *args)
139
+
140
+ run_on_exit(from, to_state, name, *args)
141
+
142
+ transition_value = persist_workflow_state to_value
143
+
144
+ run_on_entry(to_state, from, name, *args)
145
+
146
+ run_after_transition(from, to_state, name, *args)
147
+
148
+ return_value.nil? ? transition_value : return_value
149
+ end
150
+
151
+ def halt(reason = nil)
152
+ @halted_because = reason
153
+ @halted = true
154
+ end
155
+
156
+ def halt!(reason = nil)
157
+ @halted_because = reason
158
+ @halted = true
159
+ raise TransitionHalted.new(reason)
160
+ end
161
+
162
+ def spec
163
+ # check the singleton class first
164
+ class << self
165
+ return workflow_spec if workflow_spec
166
+ end
167
+
168
+ c = self.class
169
+ # using a simple loop instead of class_inheritable_accessor to avoid
170
+ # dependency on Rails' ActiveSupport
171
+ until c.workflow_spec || !(c.include? Workflow)
172
+ c = c.superclass
173
+ end
174
+ c.workflow_spec
175
+ end
176
+
177
+ private
178
+
179
+ def check_transition(event)
180
+ # Create a meaningful error message instead of
181
+ # "undefined method `on_entry' for nil:NilClass"
182
+ # Reported by Kyle Burton
183
+ if !spec.states[event.transitions_to]
184
+ raise WorkflowError.new("Event[#{event.name}]'s " +
185
+ "transitions_to[#{event.transitions_to}] is not a declared state.")
186
+ end
187
+ end
188
+
189
+ def run_before_transition(from, to, event, *args)
190
+ instance_exec(from.name, to.name, event, *args, &spec.before_transition_proc) if
191
+ spec.before_transition_proc
192
+ end
193
+
194
+ def run_on_error(error, from, to, event, *args)
195
+ if spec.on_error_proc
196
+ instance_exec(error, from.name, ( to.name rescue nil ), event, *args, &spec.on_error_proc)
197
+ halt(error.message)
198
+ else
199
+ raise error
200
+ end
201
+ end
202
+
203
+ def run_on_unavailable_transition(from, to_name, *args)
204
+ if !spec.on_unavailable_transition_proc || !instance_exec(from.name, to_name.to_sym, *args, &spec.on_unavailable_transition_proc)
205
+ run_on_error(NoTransitionAllowed.new(
206
+ "There is no event #{to_name.to_sym} defined for the #{current_state} state"),
207
+ current_state,
208
+ nil, to_name, *args)
209
+ return false
210
+ end
211
+ end
212
+
213
+ def run_on_transition(from, to, event, *args)
214
+ instance_exec(from.name, to.name, event, *args, &spec.on_transition_proc) if spec.on_transition_proc
215
+ end
216
+
217
+ def run_after_transition(from, to, event, *args)
218
+ instance_exec(from.name, to.name, event, *args, &spec.after_transition_proc) if
219
+ spec.after_transition_proc
220
+ end
221
+
222
+ def run_action(action, *args)
223
+ instance_exec(*args, &action) if action
224
+ end
225
+
226
+ def has_callback?(action)
227
+ # 1. public callback method or
228
+ # 2. protected method somewhere in the class hierarchy or
229
+ # 3. private in the immediate class (parent classes ignored)
230
+ action = action.to_sym
231
+ self.respond_to?(action) or
232
+ self.class.protected_method_defined?(action) or
233
+ self.private_methods(false).map(&:to_sym).include?(action)
234
+ end
235
+
236
+ def run_action_callback(action_name, *args)
237
+ action = action_name.to_sym
238
+ self.send(action, *args) if has_callback?(action)
239
+ end
240
+
241
+ def run_on_entry(state, prior_state, triggering_event, *args)
242
+ if state.on_entry
243
+ instance_exec(prior_state.name, triggering_event, *args, &state.on_entry)
244
+ else
245
+ hook_name = "on_#{state}_entry"
246
+ self.send hook_name, prior_state, triggering_event, *args if has_callback?(hook_name)
247
+ end
248
+ end
249
+
250
+ def run_on_exit(state, new_state, triggering_event, *args)
251
+ if state
252
+ if state.on_exit
253
+ instance_exec(new_state.name, triggering_event, *args, &state.on_exit)
254
+ else
255
+ hook_name = "on_#{state}_exit"
256
+ self.send hook_name, new_state, triggering_event, *args if has_callback?(hook_name)
257
+ end
258
+ end
259
+ end
260
+
261
+ # load_workflow_state and persist_workflow_state
262
+ # can be overriden to handle the persistence of the workflow state.
263
+ #
264
+ # Default (non ActiveRecord) implementation stores the current state
265
+ # in a variable.
266
+ #
267
+ # Default ActiveRecord implementation uses a 'workflow_state' database column.
268
+ def load_workflow_state
269
+ @workflow_state if instance_variable_defined? :@workflow_state
270
+ end
271
+
272
+ def persist_workflow_state(new_value)
273
+
274
+ @workflow_state = new_value
275
+ end
276
+ end
277
+
278
+ def self.included(klass)
279
+ klass.send :include, InstanceMethods
280
+
281
+ # backup the parent workflow spec, making accessible through #inherited_workflow_spec
282
+ if klass.superclass.respond_to?(:workflow_spec, true)
283
+ klass.module_eval do
284
+ # see http://stackoverflow.com/a/2495650/111995 for implementation explanation
285
+ pro = Proc.new { klass.superclass.workflow_spec }
286
+ singleton_class = class << self; self; end
287
+ singleton_class.send(:define_method, :inherited_workflow_spec) do
288
+ pro.call
289
+ end
290
+ end
291
+ end
292
+
293
+ klass.extend ClassMethods
294
+
295
+ # Look for a hook; otherwise detect based on ancestor class.
296
+ if klass.respond_to?(:workflow_adapter)
297
+ klass.send :include, klass.workflow_adapter
298
+ else
299
+ if Object.const_defined?(:ActiveRecord) && klass < ActiveRecord::Base
300
+ klass.send :include, Adapter::ActiveRecord
301
+ end
302
+ if Object.const_defined?(:Remodel) && klass < Adapter::Remodel::Entity
303
+ klass.send :include, Adapter::Remodel::InstanceMethods
304
+ end
305
+ end
306
+ end
307
+ end
Binary file
@@ -0,0 +1,56 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ $VERBOSE = false
4
+ require 'active_record'
5
+ require 'sqlite3'
6
+ require 'workflow'
7
+
8
+ ActiveRecord::Migration.verbose = false
9
+
10
+ class Article < ActiveRecord::Base
11
+ include Workflow
12
+
13
+ workflow do
14
+ state :new
15
+ state :accepted
16
+ end
17
+ end
18
+
19
+ class ActiveRecordScopesTest < ActiveRecordTestCase
20
+
21
+ def setup
22
+ super
23
+
24
+ ActiveRecord::Schema.define do
25
+ create_table :articles do |t|
26
+ t.string :title
27
+ t.string :body
28
+ t.string :blame_reason
29
+ t.string :reject_reason
30
+ t.string :workflow_state
31
+ end
32
+ end
33
+ end
34
+
35
+ def assert_state(title, expected_state, klass = Order)
36
+ o = klass.find_by_title(title)
37
+ assert_equal expected_state, o.read_attribute(klass.workflow_column)
38
+ o
39
+ end
40
+
41
+ test 'have "with_new_state" scope' do
42
+ assert_respond_to Article, :with_new_state
43
+ end
44
+
45
+ test 'have "with_accepted_state" scope' do
46
+ assert_respond_to Article, :with_accepted_state
47
+ end
48
+
49
+ test 'have "without_new_state" scope' do
50
+ assert_respond_to Article, :without_new_state
51
+ end
52
+
53
+ test 'have "without_accepted_state" scope' do
54
+ assert_respond_to Article, :without_accepted_state
55
+ end
56
+ end
@@ -0,0 +1,79 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ $VERBOSE = false
4
+ require 'active_record'
5
+ require 'sqlite3'
6
+ require 'workflow'
7
+
8
+ ActiveRecord::Migration.verbose = false
9
+
10
+ class EnumArticle < ActiveRecord::Base
11
+ include Workflow
12
+
13
+ workflow do
14
+ state :new, 1 do
15
+ event :accept, transitions_to: :accepted
16
+ end
17
+ state :accepted, 3
18
+ end
19
+ end
20
+
21
+ class ActiveRecordScopesWithValuesTest < ActiveRecordTestCase
22
+
23
+ def setup
24
+ super
25
+
26
+ ActiveRecord::Schema.define do
27
+ create_table :enum_articles do |t|
28
+ t.string :title
29
+ t.string :body
30
+ t.string :blame_reason
31
+ t.string :reject_reason
32
+ t.integer :workflow_state
33
+ end
34
+ end
35
+ end
36
+
37
+ test 'have "with_new_state" scope' do
38
+ assert_respond_to EnumArticle, :with_new_state
39
+ end
40
+
41
+ test '"with_new_state" selects matching value' do
42
+ article = EnumArticle.create
43
+ assert_equal(article.workflow_state, 1)
44
+ assert_equal(EnumArticle.with_new_state.all, [article])
45
+ end
46
+
47
+ test 'have "with_accepted_state" scope' do
48
+ assert_respond_to EnumArticle, :with_accepted_state
49
+ end
50
+
51
+ test '"with_accepted_state" selects matching values' do
52
+ article = EnumArticle.create
53
+ article.accept!
54
+ assert_equal(EnumArticle.with_accepted_state.all, [article])
55
+ end
56
+
57
+ test 'have "without_new_state" scope' do
58
+ assert_respond_to EnumArticle, :without_new_state
59
+ end
60
+
61
+ test '"without_new_state" filters matching value' do
62
+ article = EnumArticle.create
63
+ article.accept!
64
+ assert_equal(article.workflow_state, 3)
65
+ assert_equal(EnumArticle.without_new_state, [article])
66
+ end
67
+
68
+ test 'have "without_accepted_state" scope' do
69
+ assert_respond_to EnumArticle, :without_accepted_state
70
+ end
71
+
72
+ test '"without_accepted_state" filters matching value' do
73
+ article = EnumArticle.create
74
+ assert_equal(article.workflow_state, 1)
75
+ assert_equal(EnumArticle.without_accepted_state, [article])
76
+ end
77
+
78
+ end
79
+
@@ -0,0 +1,52 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'workflow'
3
+ class AdapterHookTest < ActiveRecordTestCase
4
+ test 'hook to choose adapter' do
5
+
6
+ ActiveRecord::Schema.define do
7
+ create_table(:examples) { |t| t.string :workflow_state }
8
+ end
9
+
10
+ class DefaultAdapter < ActiveRecord::Base
11
+ self.table_name = :examples
12
+ include Workflow
13
+ workflow do
14
+ state(:initial) { event :progress, :transitions_to => :last }
15
+ state(:last)
16
+ end
17
+ end
18
+
19
+ class ChosenByHookAdapter < ActiveRecord::Base
20
+ self.table_name = :examples
21
+ attr_reader :foo
22
+ def self.workflow_adapter
23
+ Module.new do
24
+ def load_workflow_state
25
+ @foo if defined?(@foo)
26
+ end
27
+ def persist_workflow_state(new_value)
28
+ @foo = new_value
29
+ end
30
+ end
31
+ end
32
+
33
+ include Workflow
34
+ workflow do
35
+ state(:initial) { event :progress, :transitions_to => :last }
36
+ state(:last)
37
+ end
38
+ end
39
+
40
+ default = DefaultAdapter.create
41
+ assert default.initial?
42
+ default.progress!
43
+ assert default.last?
44
+ assert DefaultAdapter.find(default.id).last?, 'should have persisted via ActiveRecord'
45
+
46
+ hook = ChosenByHookAdapter.create
47
+ assert hook.initial?
48
+ hook.progress!
49
+ assert_equal hook.foo, :last, 'should have "persisted" with custom adapter'
50
+ assert ChosenByHookAdapter.find(hook.id).initial?, 'should not have persisted via ActiveRecord'
51
+ end
52
+ end
@@ -0,0 +1,84 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'workflow'
3
+ class AdvanceExamplesTest < ActiveRecordTestCase
4
+
5
+ class Article
6
+ include Workflow
7
+ workflow do
8
+ state :new do
9
+ event :submit, :transitions_to => :awaiting_review
10
+ end
11
+ state :awaiting_review do
12
+ event :review, :transitions_to => :being_reviewed
13
+ end
14
+ state :being_reviewed do
15
+ event :accept, :transitions_to => :accepted
16
+ event :reject, :transitions_to => :rejected
17
+ end
18
+ state :accepted do
19
+ end
20
+ state :rejected do
21
+ end
22
+ end
23
+ end
24
+
25
+ test '#63 undoing event - automatically add revert events for every defined event' do
26
+ # also see https://github.com/geekq/workflow/issues/63
27
+ spec = Article.workflow_spec
28
+ spec.state_names.each do |state_name|
29
+ state = spec.states[state_name]
30
+
31
+ (state.events.flat.reject {|e| e.name.to_s =~ /^revert_/ }).each do |event|
32
+ event_name = event.name
33
+ revert_event_name = "revert_" + event_name.to_s
34
+
35
+ # Add revert events
36
+ spec.states[event.transitions_to.to_sym].events.push(
37
+ revert_event_name,
38
+ Workflow::Event.new(revert_event_name, state)
39
+ )
40
+
41
+ # Add methods for revert events
42
+ Article.module_eval do
43
+ define_method "#{revert_event_name}!".to_sym do |*args|
44
+ process_event!(revert_event_name, *args)
45
+ end
46
+ define_method "can_#{revert_event_name}?" do
47
+ return self.current_state.events.include?(revert_event_name)
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+
54
+ a = Article.new
55
+ assert(a.new?, "should start with the 'new' state")
56
+ a.submit!
57
+ assert(a.awaiting_review?, "should now be in 'awaiting_review' state")
58
+ assert_equal(['revert_submit', 'review'], a.current_state.events.keys.map(&:to_s).sort)
59
+ a.revert_submit! # this method is added by our meta programming magic above
60
+ assert(a.new?, "should now be back in the 'new' state")
61
+ end
62
+
63
+ test '#92 Load workflow specification' do
64
+ c = Class.new
65
+ c.class_eval do
66
+ include Workflow
67
+ end
68
+
69
+ # build a Specification (you can load it from yaml file too)
70
+ myspec = Workflow::Specification.new do
71
+ state :one do
72
+ event :dynamic_transition, :transitions_to => :one_a
73
+ end
74
+ state :one_a
75
+ end
76
+
77
+ c.send :assign_workflow, myspec
78
+
79
+ a = c.new
80
+ a.dynamic_transition!(1)
81
+ assert a.one_a?, 'Expected successful transition to a new state'
82
+ end
83
+
84
+ end
@@ -0,0 +1,119 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ $VERBOSE = false
4
+ require 'active_record'
5
+ require 'sqlite3'
6
+ require 'workflow'
7
+
8
+ ActiveRecord::Migration.verbose = false
9
+
10
+ # Transition based validation
11
+ # ---------------------------
12
+ # If you are using ActiveRecord you might want to define different validations
13
+ # for different transitions. There is a `validates_presence_of` hook that let's
14
+ # you specify the attributes that need to be present for an successful transition.
15
+ # If the object is not valid at the end of the transition event the transition
16
+ # is halted and a TransitionHalted exception is thrown.
17
+ #
18
+ # Here is a sample that illustrates how to use the presence validation:
19
+ # (use case suggested by http://github.com/southdesign)
20
+ class Article < ActiveRecord::Base
21
+ include Workflow
22
+ workflow do
23
+ state :new do
24
+ event :accept, :transitions_to => :accepted, :meta => {:validates_presence_of => [:title, :body]}
25
+ event :reject, :transitions_to => :rejected
26
+ end
27
+ state :accepted do
28
+ event :blame, :transitions_to => :blamed, :meta => {:validates_presence_of => [:title, :body, :blame_reason]}
29
+ event :delete, :transitions_to => :deleted
30
+ end
31
+ state :rejected do
32
+ event :delete, :transitions_to => :deleted
33
+ end
34
+ state :blamed do
35
+ event :delete, :transitions_to => :deleted
36
+ end
37
+ state :deleted do
38
+ event :accept, :transitions_to => :accepted
39
+ end
40
+
41
+ on_transition do |from, to, triggering_event, *event_args|
42
+ if self.class.superclass.to_s.split("::").first == "ActiveRecord"
43
+ singleton = class << self; self end
44
+ validations = Proc.new {}
45
+
46
+ meta = Article.workflow_spec.states[from].events[triggering_event].first.meta
47
+ fields_to_validate = meta[:validates_presence_of]
48
+ if fields_to_validate
49
+ validations = Proc.new {
50
+ errors.add_on_blank(fields_to_validate) if fields_to_validate
51
+ }
52
+ end
53
+
54
+ singleton.send :define_method, :validate_for_transition, &validations
55
+ validate_for_transition
56
+ halt! "Event[#{triggering_event}]'s transitions_to[#{to}] is not valid." unless self.errors.empty?
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ class AdvancedHooksAndValidationTest < ActiveRecordTestCase
63
+
64
+ def setup
65
+ super
66
+
67
+ ActiveRecord::Schema.define do
68
+ create_table :articles do |t|
69
+ t.string :title
70
+ t.string :body
71
+ t.string :blame_reason
72
+ t.string :reject_reason
73
+ t.string :workflow_state
74
+ end
75
+ end
76
+
77
+ exec "INSERT INTO articles(title, body, blame_reason, reject_reason, workflow_state) VALUES('new1', NULL, NULL, NULL, 'new')"
78
+ exec "INSERT INTO articles(title, body, blame_reason, reject_reason, workflow_state) VALUES('new2', 'some content', NULL, NULL, 'new')"
79
+ exec "INSERT INTO articles(title, body, blame_reason, reject_reason, workflow_state) VALUES('accepted1', 'some content', NULL, NULL, 'accepted')"
80
+
81
+ end
82
+
83
+ def assert_state(title, expected_state, klass = Order)
84
+ o = klass.find_by_title(title)
85
+ assert_equal expected_state, o.read_attribute(klass.workflow_column)
86
+ o
87
+ end
88
+
89
+ test 'deny transition from new to accepted because of the missing presence of the body' do
90
+ a = Article.find_by_title('new1');
91
+ assert_raise Workflow::TransitionHalted do
92
+ a.accept!
93
+ end
94
+ assert_state 'new1', 'new', Article
95
+ end
96
+
97
+ test 'allow transition from new to accepted because body is present this time' do
98
+ a = Article.find_by_title('new2');
99
+ assert a.accept!
100
+ assert_state 'new2', 'accepted', Article
101
+ end
102
+
103
+ test 'allow transition from accepted to blamed because of a blame_reason' do
104
+ a = Article.find_by_title('accepted1');
105
+ a.blame_reason = "Provocant thesis"
106
+ assert a.blame!
107
+ assert_state 'accepted1', 'blamed', Article
108
+ end
109
+
110
+ test 'deny transition from accepted to blamed because of no blame_reason' do
111
+ a = Article.find_by_title('accepted1');
112
+ assert_raise Workflow::TransitionHalted do
113
+ assert a.blame!
114
+ end
115
+ assert_state 'accepted1', 'accepted', Article
116
+ end
117
+
118
+ end
119
+