workflow-rails4 1.1.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,31 @@
1
+ require 'rubygems'
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+
5
+ require 'bundler'
6
+ Bundler.setup
7
+ Bundler::GemHelper.install_tasks
8
+
9
+ task :default => [:test]
10
+
11
+ require 'rake'
12
+ Rake::TestTask.new do |t|
13
+ t.libs << 'test'
14
+ t.verbose = true
15
+ t.warning = true
16
+ t.test_files = FileList['test/*_test.rb'] + FileList['test/new_versions/*_test.rb']
17
+ end
18
+
19
+ Rake::TestTask.new do |t|
20
+ t.name = 'test_without_new_versions'
21
+ t.libs << 'test'
22
+ t.verbose = true
23
+ t.warning = true
24
+ t.pattern = 'test/*_test.rb'
25
+ end
26
+
27
+ Rake::RDocTask.new do |rdoc|
28
+ rdoc.rdoc_files.include("lib/**/*.rb")
29
+ rdoc.options << "-S"
30
+ end
31
+
@@ -0,0 +1,11 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem "rdoc", ">= 3.12"
5
+ gem "bundler", ">= 1.0.0"
6
+ gem "activerecord", "~>2.3.15"
7
+ gem "sqlite3"
8
+ gem "mocha"
9
+ gem "rake"
10
+ gem "ruby-graphviz", ">= 1.0"
11
+ end
@@ -0,0 +1,11 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem "rdoc", ">= 3.12"
5
+ gem "bundler", ">= 1.0.0"
6
+ gem "activerecord", "~>3.2"
7
+ gem "sqlite3"
8
+ gem "mocha"
9
+ gem "rake"
10
+ gem "ruby-graphviz", ">= 1.0"
11
+ end
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem "rdoc", ">= 3.12"
5
+ gem "bundler", ">= 1.0.0"
6
+ gem "activerecord"
7
+ gem "sqlite3"
8
+ gem "mocha"
9
+ gem "rake"
10
+ gem "ruby-graphviz", ">= 1.0"
11
+ gem 'protected_attributes'
12
+ end
@@ -0,0 +1,277 @@
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
+ if column_name
14
+ @workflow_state_column_name = column_name.to_sym
15
+ end
16
+ if !@workflow_state_column_name && superclass.respond_to?(:workflow_column)
17
+ @workflow_state_column_name = superclass.workflow_column
18
+ end
19
+ @workflow_state_column_name ||= :workflow_state
20
+ end
21
+
22
+ def workflow(&specification)
23
+ assign_workflow Specification.new(Hash.new, &specification)
24
+ end
25
+
26
+ private
27
+
28
+ # Creates the convinience methods like `my_transition!`
29
+ def assign_workflow(specification_object)
30
+
31
+ # Merging two workflow specifications can **not** be done automically, so
32
+ # just make the latest specification win. Same for inheritance -
33
+ # definition in the subclass wins.
34
+ if respond_to? :inherited_workflow_spec # undefine methods defined by the old workflow_spec
35
+ inherited_workflow_spec.states.values.each do |state|
36
+ state_name = state.name
37
+ module_eval do
38
+ undef_method "#{state_name}?"
39
+ end
40
+
41
+ state.events.values.each do |event|
42
+ event_name = event.name
43
+ module_eval do
44
+ undef_method "#{event_name}!".to_sym
45
+ undef_method "can_#{event_name}?"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ @workflow_spec = specification_object
52
+ @workflow_spec.states.values.each do |state|
53
+ state_name = state.name
54
+ module_eval do
55
+ define_method "#{state_name}?" do
56
+ state_name == current_state.name
57
+ end
58
+ end
59
+
60
+ state.events.values.each do |event|
61
+ event_name = event.name
62
+ module_eval do
63
+ define_method "#{event_name}!".to_sym do |*args|
64
+ process_event!(event_name, *args)
65
+ end
66
+
67
+ define_method "can_#{event_name}?" do
68
+ return self.current_state.events.include?(event_name)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ module InstanceMethods
77
+
78
+ def current_state
79
+ loaded_state = load_workflow_state
80
+ res = spec.states[loaded_state.to_sym] if loaded_state
81
+ res || spec.initial_state
82
+ end
83
+
84
+ # See the 'Guards' section in the README
85
+ # @return true if the last transition was halted by one of the transition callbacks.
86
+ def halted?
87
+ @halted
88
+ end
89
+
90
+ # @return the reason of the last transition abort as set by the previous
91
+ # call of `halt` or `halt!` method.
92
+ def halted_because
93
+ @halted_because
94
+ end
95
+
96
+ def process_event!(name, *args)
97
+ event = current_state.events[name.to_sym]
98
+ raise NoTransitionAllowed.new(
99
+ "There is no event #{name.to_sym} defined for the #{current_state} state") \
100
+ if event.nil?
101
+ @halted_because = nil
102
+ @halted = false
103
+
104
+ check_transition(event)
105
+
106
+ from = current_state
107
+ to = spec.states[event.transitions_to]
108
+
109
+ run_before_transition(from, to, name, *args)
110
+ return false if @halted
111
+
112
+ begin
113
+ return_value = run_action(event.action, *args) || run_action_callback(event.name, *args)
114
+ rescue Exception => e
115
+ run_on_error(e, from, to, name, *args)
116
+ end
117
+
118
+ return false if @halted
119
+
120
+ run_on_transition(from, to, name, *args)
121
+
122
+ run_on_exit(from, to, name, *args)
123
+
124
+ transition_value = persist_workflow_state to.to_s
125
+
126
+ run_on_entry(to, from, name, *args)
127
+
128
+ run_after_transition(from, to, name, *args)
129
+
130
+ return_value.nil? ? transition_value : return_value
131
+ end
132
+
133
+ def halt(reason = nil)
134
+ @halted_because = reason
135
+ @halted = true
136
+ end
137
+
138
+ def halt!(reason = nil)
139
+ @halted_because = reason
140
+ @halted = true
141
+ raise TransitionHalted.new(reason)
142
+ end
143
+
144
+ def spec
145
+ # check the singleton class first
146
+ class << self
147
+ return workflow_spec if workflow_spec
148
+ end
149
+
150
+ c = self.class
151
+ # using a simple loop instead of class_inheritable_accessor to avoid
152
+ # dependency on Rails' ActiveSupport
153
+ until c.workflow_spec || !(c.include? Workflow)
154
+ c = c.superclass
155
+ end
156
+ c.workflow_spec
157
+ end
158
+
159
+ private
160
+
161
+ def check_transition(event)
162
+ # Create a meaningful error message instead of
163
+ # "undefined method `on_entry' for nil:NilClass"
164
+ # Reported by Kyle Burton
165
+ if !spec.states[event.transitions_to]
166
+ raise WorkflowError.new("Event[#{event.name}]'s " +
167
+ "transitions_to[#{event.transitions_to}] is not a declared state.")
168
+ end
169
+ end
170
+
171
+ def run_before_transition(from, to, event, *args)
172
+ instance_exec(from.name, to.name, event, *args, &spec.before_transition_proc) if
173
+ spec.before_transition_proc
174
+ end
175
+
176
+ def run_on_error(error, from, to, event, *args)
177
+ if spec.on_error_proc
178
+ instance_exec(error, from.name, to.name, event, *args, &spec.on_error_proc)
179
+ halt(error.message)
180
+ else
181
+ raise error
182
+ end
183
+ end
184
+
185
+ def run_on_transition(from, to, event, *args)
186
+ instance_exec(from.name, to.name, event, *args, &spec.on_transition_proc) if spec.on_transition_proc
187
+ end
188
+
189
+ def run_after_transition(from, to, event, *args)
190
+ instance_exec(from.name, to.name, event, *args, &spec.after_transition_proc) if
191
+ spec.after_transition_proc
192
+ end
193
+
194
+ def run_action(action, *args)
195
+ instance_exec(*args, &action) if action
196
+ end
197
+
198
+ def has_callback?(action)
199
+ # 1. public callback method or
200
+ # 2. protected method somewhere in the class hierarchy or
201
+ # 3. private in the immediate class (parent classes ignored)
202
+ self.respond_to?(action) or
203
+ self.class.protected_method_defined?(action) or
204
+ self.private_methods(false).map(&:to_sym).include?(action)
205
+ end
206
+
207
+ def run_action_callback(action_name, *args)
208
+ action = action_name.to_sym
209
+ self.send(action, *args) if has_callback?(action)
210
+ end
211
+
212
+ def run_on_entry(state, prior_state, triggering_event, *args)
213
+ if state.on_entry
214
+ instance_exec(prior_state.name, triggering_event, *args, &state.on_entry)
215
+ else
216
+ hook_name = "on_#{state}_entry"
217
+ self.send hook_name, prior_state, triggering_event, *args if self.respond_to? hook_name
218
+ end
219
+ end
220
+
221
+ def run_on_exit(state, new_state, triggering_event, *args)
222
+ if state
223
+ if state.on_exit
224
+ instance_exec(new_state.name, triggering_event, *args, &state.on_exit)
225
+ else
226
+ hook_name = "on_#{state}_exit"
227
+ self.send hook_name, new_state, triggering_event, *args if self.respond_to? hook_name
228
+ end
229
+ end
230
+ end
231
+
232
+ # load_workflow_state and persist_workflow_state
233
+ # can be overriden to handle the persistence of the workflow state.
234
+ #
235
+ # Default (non ActiveRecord) implementation stores the current state
236
+ # in a variable.
237
+ #
238
+ # Default ActiveRecord implementation uses a 'workflow_state' database column.
239
+ def load_workflow_state
240
+ @workflow_state if instance_variable_defined? :@workflow_state
241
+ end
242
+
243
+ def persist_workflow_state(new_value)
244
+ @workflow_state = new_value
245
+ end
246
+ end
247
+
248
+ def self.included(klass)
249
+ klass.send :include, InstanceMethods
250
+
251
+ # backup the parent workflow spec, making accessible through #inherited_workflow_spec
252
+ if klass.superclass.respond_to?(:workflow_spec, true)
253
+ klass.module_eval do
254
+ # see http://stackoverflow.com/a/2495650/111995 for implementation explanation
255
+ pro = Proc.new { klass.superclass.workflow_spec }
256
+ singleton_class = class << self; self; end
257
+ singleton_class.send(:define_method, :inherited_workflow_spec) do
258
+ pro.call
259
+ end
260
+ end
261
+ end
262
+
263
+ klass.extend ClassMethods
264
+
265
+ if Object.const_defined?(:ActiveRecord)
266
+ if klass < ActiveRecord::Base
267
+ klass.send :include, Adapter::ActiveRecord::InstanceMethods
268
+ klass.send :extend, Adapter::ActiveRecord::Scopes
269
+ klass.before_validation :write_initial_state
270
+ end
271
+ elsif Object.const_defined?(:Remodel)
272
+ if klass < Adapter::Remodel::Entity
273
+ klass.send :include, Remodel::InstanceMethods
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,66 @@
1
+ module Workflow
2
+ module Adapter
3
+ module ActiveRecord
4
+ module InstanceMethods
5
+ def load_workflow_state
6
+ read_attribute(self.class.workflow_column)
7
+ end
8
+
9
+ # On transition the new workflow state is immediately saved in the
10
+ # database.
11
+ def persist_workflow_state(new_value)
12
+ if self.respond_to? :update_column
13
+ # Rails 3.1 or newer
14
+ update_column self.class.workflow_column, new_value
15
+ else
16
+ # older Rails; beware of side effect: other (pending) attribute changes will be persisted too
17
+ update_attribute self.class.workflow_column, new_value
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ # Motivation: even if NULL is stored in the workflow_state database column,
24
+ # the current_state is correctly recognized in the Ruby code. The problem
25
+ # arises when you want to SELECT records filtering by the value of initial
26
+ # state. That's why it is important to save the string with the name of the
27
+ # initial state in all the new records.
28
+ def write_initial_state
29
+ write_attribute self.class.workflow_column, current_state.to_s
30
+ end
31
+ end
32
+
33
+ # This module will automatically generate ActiveRecord scopes based on workflow states.
34
+ # The name of each generated scope will be something like `with_<state_name>_state`
35
+ #
36
+ # Examples:
37
+ #
38
+ # Article.with_pending_state # => ActiveRecord::Relation
39
+ #
40
+ # Example above just adds `where(:state_column_name => 'pending')` to AR query and returns
41
+ # ActiveRecord::Relation.
42
+ module Scopes
43
+ def self.extended(object)
44
+ class << object
45
+ alias_method :workflow_without_scopes, :workflow unless method_defined?(:workflow_without_scopes)
46
+ alias_method :workflow, :workflow_with_scopes
47
+ end
48
+ end
49
+
50
+ def workflow_with_scopes(&specification)
51
+ workflow_without_scopes(&specification)
52
+ states = workflow_spec.states.values
53
+ eigenclass = class << self; self; end
54
+
55
+ states.each do |state|
56
+ # Use eigenclass instead of `define_singleton_method`
57
+ # to be compatible with Ruby 1.8+
58
+ eigenclass.send(:define_method, "with_#{state}_state") do
59
+ where("#{table_name}.#{self.workflow_column.to_sym} = ?", state.to_s)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ 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,79 @@
1
+ begin
2
+ require 'rubygems'
3
+
4
+ gem 'ruby-graphviz', '>=1.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.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