rails-workflow 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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