rails-workflow 1.4.0

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,110 @@
1
+ require 'active_support/concern'
2
+
3
+ module Workflow
4
+ module Adapter
5
+ module ActiveRecordValidations
6
+ extend ActiveSupport::Concern
7
+
8
+ ###
9
+ #
10
+ # Captures instance method calls of the form `:transitioning_from_<state_name>`
11
+ # and `:transitioning_to_<state_name>`.
12
+ #
13
+ # For use with validators, e.g. `validates :foobar, presence: true, if: :transitioning_to_some_state?`
14
+ #
15
+ def method_missing(method, *args, &block)
16
+ if method.to_s =~ /^transitioning_(from|to|via_event)_([\w_-]+)\?$/
17
+ class_eval "
18
+ def #{method}
19
+ transitioning? direction: '#{$~[1]}', state: '#{$~[2]}'
20
+ end
21
+ "
22
+ send method
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ def valid?(context=nil)
29
+ if errors.any?
30
+ false
31
+ else
32
+ super
33
+ end
34
+ end
35
+
36
+ def can_transition?(event_id)
37
+ event = current_state.find_event(event_id)
38
+ return false unless event
39
+
40
+ from = current_state.name
41
+ to = event.evaluate(self)
42
+
43
+ return false unless to
44
+
45
+ within_transition(from, to, event_id) do
46
+ valid?
47
+ end
48
+ end
49
+
50
+ ###
51
+ #
52
+ # Executes the given block within a context that is able to give
53
+ # correct answers to the questions, `:transitioning_from_<old_state>?`.
54
+ # `:transitioning_to_<new_state>`, `:transitioning_via_event_<event_name>?`
55
+ #
56
+ # For use with validators, e.g. `validates :foobar, presence: true, if: :transitioning_to_some_state?`
57
+ #
58
+ # = Example:
59
+ #
60
+ # before_transition do |from, to, name, *args|
61
+ # @halted = !within_transition from, to, name do
62
+ # valid?
63
+ # end
64
+ # end
65
+ #
66
+ def within_transition(from, to, event, &block)
67
+ begin
68
+ @transition_context = TransitionContext.new \
69
+ from: from,
70
+ to: to,
71
+ event: event,
72
+ attributes: {},
73
+ event_args: []
74
+
75
+ return block.call()
76
+ ensure
77
+ @transition_context = nil
78
+ end
79
+ end
80
+
81
+ module ClassMethods
82
+ def halt_transition_unless_valid!
83
+ before_transition unless: :valid? do |model|
84
+ throw :abort
85
+ end
86
+ end
87
+
88
+ def wrap_transition_in_transaction!
89
+ around_transition do |model, transition|
90
+ model.with_lock do
91
+ transition.call
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def transitioning?(direction:, state:)
100
+ state = state.to_sym
101
+ return false unless transition_context
102
+ case direction
103
+ when 'from' then transition_context.from == state
104
+ when 'to' then transition_context.to == state
105
+ else transition_context.event == state
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,15 @@
1
+ module Workflow
2
+ module Adapter
3
+ module Remodel
4
+ module InstanceMethods
5
+ def load_workflow_state
6
+ send(self.class.workflow_column)
7
+ end
8
+
9
+ def persist_workflow_state(new_value)
10
+ update(self.class.workflow_column => new_value)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,274 @@
1
+ module Workflow
2
+ module Callbacks
3
+
4
+ extend ActiveSupport::Concern
5
+ include ActiveSupport::Callbacks
6
+
7
+ CALLBACK_MAP = {
8
+ transition: :event,
9
+ exit: :from,
10
+ enter: :to
11
+ }.freeze
12
+
13
+ # @!attribute [r] transition_context
14
+ # During state transition events, contains a {TransitionContext} representing the transition underway.
15
+ # @return [TransitionContext] representation of current state transition
16
+ #
17
+ included do
18
+ attr_reader :transition_context
19
+ CALLBACK_MAP.keys.each do |type|
20
+ define_callbacks type,
21
+ skip_after_callbacks_if_terminated: true
22
+ end
23
+ end
24
+
25
+ module ClassMethods
26
+ ##
27
+ # @!method before_transition
28
+ #
29
+ # :call-seq:
30
+ # before_transition(*instance_method_names, options={})
31
+ # before_transition(options={}, &block)
32
+ #
33
+ # Append a callback before transition.
34
+ # Instance methods used for `before` and `after` transitions
35
+ # receive no parameters. Instance methods for `around` transitions will be given a block,
36
+ # which must be yielded/called in order for the sequence to continue.
37
+ #
38
+ # Using a block notation, the first parameter will be an instance of the object
39
+ # under transition, while the second parameter (`around` transition only) will be
40
+ # the block which should be called for the sequence to continue.
41
+ #
42
+ # == Transition Metadata
43
+ #
44
+ # Within the callback you can access the `transition_context` instance variable,
45
+ # which will give you metadata and arguments passed to the transition.
46
+ # See Workflow::TransitionContext
47
+ #
48
+ # == Options
49
+ #
50
+ # === If / Unless
51
+ #
52
+ # The callback will run `if` or `unless` the named method returns a truthy value.
53
+ #
54
+ # # Assuming some_instance_method returns a boolean,
55
+ # before_transition :do_something, if: :some_instance_method
56
+ # before_transition :do_something_else, unless: :some_instance_method
57
+ #
58
+ # === Only / Except
59
+ #
60
+ # The callback will run `if` or `unless` the event being processed is in the list given
61
+ #
62
+ # # Run this callback only on the `accept` and `publish` events.
63
+ # before_transition :do_something, only: [:accept, :publish]
64
+ # # Run this callback on events other than the `accept` and `publish` events.
65
+ # before_transition :do_something_else, except: [:accept, :publish]
66
+ #
67
+
68
+ ##
69
+ # @!method prepend_before_transition(*instance_method_names, options={})
70
+ #
71
+ # Something Interesting
72
+ #
73
+ # @overload prepend_before_transition(options={}, &block)
74
+ #
75
+ # Prepend a callback before transition, making it the first before transition called.
76
+ # Options are the same as for the standard #before_transition method.
77
+
78
+ ##
79
+ # @!method skip_before_transition
80
+ #
81
+ # :call-seq: skip_before_transition(names)
82
+ #
83
+ # Skip a callback before transition.
84
+ # Options are the same as for the standard #before_transition method.
85
+
86
+ ##
87
+ # @!method after_transition
88
+ #
89
+ # :call-seq:
90
+ # after_transition(*instance_method_names, options={})
91
+ # after_transition(options={}, &block)
92
+ #
93
+ # Append a callback after transition.
94
+ # Instance methods used for `before` and `after` transitions
95
+ # receive no parameters. Instance methods for `around` transitions will be given a block,
96
+ # which must be yielded/called in order for the sequence to continue.
97
+ #
98
+ # Using a block notation, the first parameter will be an instance of the object
99
+ # under transition, while the second parameter (`around` transition only) will be
100
+ # the block which should be called for the sequence to continue.
101
+ #
102
+ # == Transition Metadata
103
+ #
104
+ # Within the callback you can access the `transition_context` instance variable,
105
+ # which will give you metadata and arguments passed to the transition.
106
+ # See Workflow::TransitionContext
107
+ #
108
+ # == Options
109
+ #
110
+ # === If / Unless
111
+ #
112
+ # The callback will run `if` or `unless` the named method returns a truthy value.
113
+ #
114
+ # # Assuming some_instance_method returns a boolean,
115
+ # after_transition :do_something, if: :some_instance_method
116
+ # after_transition :do_something_else, unless: :some_instance_method
117
+ #
118
+ # === Only / Except
119
+ #
120
+ # The callback will run `if` or `unless` the event being processed is in the list given
121
+ #
122
+ # # Run this callback only on the `accept` and `publish` events.
123
+ # after_transition :do_something, only: [:accept, :publish]
124
+ # # Run this callback on events other than the `accept` and `publish` events.
125
+ # after_transition :do_something_else, except: [:accept, :publish]
126
+ #
127
+
128
+ ##
129
+ # @!method prepend_after_transition(*instance_method_names, options={})
130
+ #
131
+ # Something Interesting
132
+ #
133
+ # @overload prepend_after_transition(options={}, &block)
134
+ #
135
+ # Prepend a callback after transition, making it the first after transition called.
136
+ # Options are the same as for the standard #after_transition method.
137
+
138
+ ##
139
+ # @!method skip_after_transition
140
+ #
141
+ # :call-seq: skip_after_transition(names)
142
+ #
143
+ # Skip a callback after transition.
144
+ # Options are the same as for the standard #after_transition method.
145
+
146
+ ##
147
+ # @!method around_transition
148
+ #
149
+ # :call-seq:
150
+ # around_transition(*instance_method_names, options={})
151
+ # around_transition(options={}, &block)
152
+ #
153
+ # Append a callback around transition.
154
+ # Instance methods used for `before` and `after` transitions
155
+ # receive no parameters. Instance methods for `around` transitions will be given a block,
156
+ # which must be yielded/called in order for the sequence to continue.
157
+ #
158
+ # Using a block notation, the first parameter will be an instance of the object
159
+ # under transition, while the second parameter (`around` transition only) will be
160
+ # the block which should be called for the sequence to continue.
161
+ #
162
+ # == Transition Metadata
163
+ #
164
+ # Within the callback you can access the `transition_context` instance variable,
165
+ # which will give you metadata and arguments passed to the transition.
166
+ # See Workflow::TransitionContext
167
+ #
168
+ # == Options
169
+ #
170
+ # === If / Unless
171
+ #
172
+ # The callback will run `if` or `unless` the named method returns a truthy value.
173
+ #
174
+ # # Assuming some_instance_method returns a boolean,
175
+ # around_transition :do_something, if: :some_instance_method
176
+ # around_transition :do_something_else, unless: :some_instance_method
177
+ #
178
+ # === Only / Except
179
+ #
180
+ # The callback will run `if` or `unless` the event being processed is in the list given
181
+ #
182
+ # # Run this callback only on the `accept` and `publish` events.
183
+ # around_transition :do_something, only: [:accept, :publish]
184
+ # # Run this callback on events other than the `accept` and `publish` events.
185
+ # around_transition :do_something_else, except: [:accept, :publish]
186
+ #
187
+
188
+ ##
189
+ # @!method prepend_around_transition(*instance_method_names, options={})
190
+ #
191
+ # Something Interesting
192
+ #
193
+ # @overload prepend_around_transition(options={}, &block)
194
+ #
195
+ # Prepend a callback around transition, making it the first around transition called.
196
+ # Options are the same as for the standard #around_transition method.
197
+
198
+ ##
199
+ # @!method skip_around_transition
200
+ #
201
+ # :call-seq: skip_around_transition(names)
202
+ #
203
+ # Skip a callback around transition.
204
+ # Options are the same as for the standard #around_transition method.
205
+
206
+ [:before, :after, :around].each do |callback|
207
+ CALLBACK_MAP.each do |type, context_attribute|
208
+ define_method "#{callback}_#{type}" do |*names, &blk|
209
+ _insert_callbacks(names, context_attribute, blk) do |name, options|
210
+ set_callback(type, callback, name, options)
211
+ end
212
+ end
213
+
214
+ define_method "prepend_#{callback}_#{type}" do |*names, &blk|
215
+ _insert_callbacks(names, context_attribute, blk) do |name, options|
216
+ set_callback(type, callback, name, options.merge(prepend: true))
217
+ end
218
+ end
219
+
220
+ # Skip a before, after or around callback. See _insert_callbacks
221
+ # for details on the allowed parameters.
222
+ define_method "skip_#{callback}_#{type}" do |*names|
223
+ _insert_callbacks(names, context_attribute) do |name, options|
224
+ skip_callback(type, callback, name, options)
225
+ end
226
+ end
227
+
228
+ # *_action is the same as append_*_action
229
+ alias_method :"append_#{callback}_#{type}", :"#{callback}_#{type}"
230
+ end
231
+ end
232
+
233
+ private
234
+ def _insert_callbacks(callbacks, context_attribute, block = nil)
235
+ options = callbacks.extract_options!
236
+ _normalize_callback_options(options, context_attribute)
237
+ callbacks.push(block) if block
238
+ callbacks.each do |callback|
239
+ yield callback, options
240
+ end
241
+ end
242
+
243
+ def _normalize_callback_options(options, context_attribute)
244
+ _normalize_callback_option(options, context_attribute, :only, :if)
245
+ _normalize_callback_option(options, context_attribute, :except, :unless)
246
+ end
247
+
248
+ def _normalize_callback_option(options, context_attribute, from, to) # :nodoc:
249
+ if from = options[from]
250
+ _from = Array(from).map(&:to_sym).to_set
251
+ from = proc { |record|
252
+ _from.include? record.transition_context.send(context_attribute).to_sym
253
+ }
254
+ options[to] = Array(options[to]).unshift(from)
255
+ end
256
+ end
257
+ end
258
+
259
+ private
260
+ # TODO: Do something here.
261
+ def halted_callback_hook(filter)
262
+ end
263
+
264
+ def run_all_callbacks(&block)
265
+ catch(:abort) do
266
+ run_callbacks :transition do
267
+ throw(:abort) if false == run_callbacks(:exit) do
268
+ throw(:abort) if false == run_callbacks(:enter, &block)
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,10 @@
1
+ module Workflow
2
+ class Configuration
3
+ attr_accessor :persist_workflow_state_immediately, :touch_on_update_column
4
+
5
+ def initialize
6
+ self.persist_workflow_state_immediately = true
7
+ self.touch_on_update_column = false
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,79 @@
1
+ begin
2
+ require 'rubygems'
3
+
4
+ gem 'ruby-graphviz', '~> 1.0.0'
5
+ gem 'activesupport'
6
+
7
+ require 'graphviz'
8
+ require 'active_support/inflector'
9
+ rescue LoadError => e
10
+ $stderr.puts "Could not load the ruby-graphiz or active_support gems for rendering: #{e.message}"
11
+ end
12
+
13
+ module Workflow
14
+ module Draw
15
+
16
+ # Generates a `dot` graph of the workflow.
17
+ # Prerequisite: the `dot` binary. (Download from http://www.graphviz.org/)
18
+ # You can use this method in your own Rakefile like this:
19
+ #
20
+ # namespace :doc do
21
+ # desc "Generate a workflow graph for a model passed e.g. as 'MODEL=Order'."
22
+ # task :workflow => :environment do
23
+ # require 'workflow/draw'
24
+ # Workflow::Draw::workflow_diagram(ENV['MODEL'].constantize)
25
+ # end
26
+ # end
27
+ #
28
+ # You can influence the placement of nodes by specifying
29
+ # additional meta information in your states and transition descriptions.
30
+ # You can assign higher `weight` value to the typical transitions
31
+ # in your workflow. All other states and transitions will be arranged
32
+ # around that main line. See also `weight` in the graphviz documentation.
33
+ # Example:
34
+ #
35
+ # state :new do
36
+ # event :approve, :transitions_to => :approved, :meta => {:weight => 8}
37
+ # end
38
+ #
39
+ #
40
+ # @param klass A class with the Workflow mixin, for which you wish the graphical workflow representation
41
+ # @param [String] target_dir Directory, where to save the dot and the pdf files
42
+ # @param [String] graph_options You can change graph orientation, size etc. See graphviz documentation
43
+ def self.workflow_diagram(klass, options={})
44
+ options = {
45
+ :name => "#{klass.name.tableize}_workflow".gsub('/', '_'),
46
+ :path => '.',
47
+ :orientation => "landscape",
48
+ :ratio => "fill",
49
+ :format => 'png',
50
+ :font => 'Helvetica'
51
+ }.merge options
52
+
53
+ graph = ::GraphViz.new('G', :rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB', :ratio => options[:ratio])
54
+
55
+ # Add nodes
56
+ klass.workflow_spec.states.each do |_, state|
57
+ node = state.draw(graph)
58
+ node.fontname = options[:font]
59
+
60
+ state.uniq_events.each do |event|
61
+ edge = event.draw(graph, state)
62
+ edge.fontname = options[:font]
63
+ end
64
+ end
65
+
66
+ # Generate the graph
67
+ filename = File.join(options[:path], "#{options[:name]}.#{options[:format]}")
68
+
69
+ graph.output options[:format] => "'#{filename}'"
70
+
71
+ puts "
72
+ Please run the following to open the generated file:
73
+
74
+ open '#{filename}'
75
+ "
76
+ graph
77
+ end
78
+ end
79
+ end