aq1018-acts_as_multiple_state_machines 0.1
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.
- data/History.txt +6 -0
- data/Manifest.txt +16 -0
- data/README.txt +48 -0
- data/Rakefile +68 -0
- data/lib/acts/as/multiple/state_machines/active_record_extension.rb +86 -0
- data/lib/acts/as/multiple/state_machines/exceptions.rb +17 -0
- data/lib/acts/as/multiple/state_machines/supporting_classes.rb +12 -0
- data/lib/acts/as/multiple/state_machines/supporting_classes/event.rb +46 -0
- data/lib/acts/as/multiple/state_machines/supporting_classes/state.rb +36 -0
- data/lib/acts/as/multiple/state_machines/supporting_classes/state_machine.rb +112 -0
- data/lib/acts/as/multiple/state_machines/supporting_classes/state_machine_factory.rb +25 -0
- data/lib/acts/as/multiple/state_machines/supporting_classes/state_transition.rb +47 -0
- data/lib/acts_as_multiple_state_machines.rb +9 -0
- data/rails/init.rb +13 -0
- data/test/fixtures/conversations.yml +11 -0
- data/test/test_acts_as_multiple_state_machines.rb +697 -0
- metadata +72 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
rails/init.rb
|
6
|
+
lib/acts_as_multiple_state_machines.rb
|
7
|
+
lib/acts/as/multiple/state_machines/active_record_extension.rb
|
8
|
+
lib/acts/as/multiple/state_machines/exceptions.rb
|
9
|
+
lib/acts/as/multiple/state_machines/supporting_classes.rb
|
10
|
+
lib/acts/as/multiple/state_machines/supporting_classes/event.rb
|
11
|
+
lib/acts/as/multiple/state_machines/supporting_classes/state.rb
|
12
|
+
lib/acts/as/multiple/state_machines/supporting_classes/state_machine.rb
|
13
|
+
lib/acts/as/multiple/state_machines/supporting_classes/state_machine_factory.rb
|
14
|
+
lib/acts/as/multiple/state_machines/supporting_classes/state_transition.rb
|
15
|
+
test/test_acts_as_multiple_state_machines.rb
|
16
|
+
test/fixtures/conversations.yml
|
data/README.txt
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
= ActsAsMultipleStateMachines
|
2
|
+
|
3
|
+
* FIX (url)
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
FIX (describe your package)
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* FIX (list of features or problems)
|
12
|
+
|
13
|
+
== SYNOPSIS:
|
14
|
+
|
15
|
+
FIX (code sample of usage)
|
16
|
+
|
17
|
+
== REQUIREMENTS:
|
18
|
+
|
19
|
+
* FIX (list of requirements)
|
20
|
+
|
21
|
+
== INSTALL:
|
22
|
+
|
23
|
+
* FIX (sudo gem install, anything else)
|
24
|
+
|
25
|
+
== LICENSE:
|
26
|
+
|
27
|
+
(The MIT License)
|
28
|
+
|
29
|
+
Copyright (c) 2009 FIX
|
30
|
+
|
31
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
32
|
+
a copy of this software and associated documentation files (the
|
33
|
+
'Software'), to deal in the Software without restriction, including
|
34
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
35
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
36
|
+
permit persons to whom the Software is furnished to do so, subject to
|
37
|
+
the following conditions:
|
38
|
+
|
39
|
+
The above copyright notice and this permission notice shall be
|
40
|
+
included in all copies or substantial portions of the Software.
|
41
|
+
|
42
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
43
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
44
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
45
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
46
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
47
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
48
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hoe'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
require 'pathname'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'rake/rdoctask'
|
7
|
+
|
8
|
+
require 'lib/acts_as_multiple_state_machines.rb'
|
9
|
+
|
10
|
+
AUTHOR = "Aaron Qian"
|
11
|
+
EMAIL = "aaron [a] ekohe [d] com"
|
12
|
+
GEM_NAME = "acts_as_multiple_state_machines"
|
13
|
+
GEM_VERSION = Acts::As::Multiple::StateMachines::VERSION
|
14
|
+
|
15
|
+
GEM_CLEAN = ["log", "pkg"]
|
16
|
+
GEM_EXTRAS = { :has_rdoc => true }
|
17
|
+
|
18
|
+
PROJECT_NAME = "acts_as_multiple_state_machines"
|
19
|
+
PROJECT_URL = "http://acts-as-multiple-state-machines.rubyforge.com"
|
20
|
+
PROJECT_DESCRIPTION = PROJECT_SUMMARY = "similar to acts_as_state_machine, but provides multiple State Machines per ActiveRecord Model"
|
21
|
+
|
22
|
+
Hoe.new(GEM_NAME, GEM_VERSION) do |p|
|
23
|
+
p.rubyforge_name = "acts-as-multiple-state-machines"
|
24
|
+
p.developer('Aaron Qian', 'aaron [a] ekohe [d] com')
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'Default: run unit tests.'
|
28
|
+
task :default => [:clean_db, :test]
|
29
|
+
|
30
|
+
WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
|
31
|
+
SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
|
32
|
+
|
33
|
+
desc "Install #{GEM_NAME} #{GEM_VERSION}"
|
34
|
+
task :install => [ :package ] do
|
35
|
+
sh "#{SUDO} gem install --local pkg/#{GEM_NAME}-#{GEM_VERSION} --no-update-sources", :verbose => false
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Uninstall #{GEM_NAME} #{GEM_VERSION} (default ruby)"
|
39
|
+
task :uninstall => [ :clobber ] do
|
40
|
+
sh "#{SUDO} gem uninstall #{GEM_NAME} -v#{GEM_VERSION} -I -x", :verbose => false
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'Run specifications'
|
44
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
45
|
+
t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
|
46
|
+
t.spec_files = Pathname.glob(Pathname.new(__FILE__).dirname + 'spec/**/*_spec.rb')
|
47
|
+
|
48
|
+
begin
|
49
|
+
t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
|
50
|
+
t.rcov_opts << '--exclude' << 'spec'
|
51
|
+
t.rcov_opts << '--text-summary'
|
52
|
+
t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
|
53
|
+
rescue Exception
|
54
|
+
# rcov not installed
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
desc 'Remove the stale db file'
|
59
|
+
task :clean_db do
|
60
|
+
`rm -f #{File.dirname(__FILE__)}/test/state_machine.sqlite.db`
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'Test the acts as multiple state machines plugin.'
|
64
|
+
Rake::TestTask.new(:test) do |t|
|
65
|
+
t.libs << 'lib'
|
66
|
+
t.pattern = 'test/**/test_*.rb'
|
67
|
+
t.verbose = true
|
68
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Acts
|
2
|
+
module As
|
3
|
+
module Multiple
|
4
|
+
module StateMachines
|
5
|
+
def self.included(base) #:nodoc:
|
6
|
+
base.extend ActMacro
|
7
|
+
end
|
8
|
+
|
9
|
+
module ActMacro
|
10
|
+
# Configuration options are
|
11
|
+
#
|
12
|
+
# * +column+ - specifies the column name to use for keeping the state (default: state)
|
13
|
+
# * +initial+ - specifies an initial state for newly created objects (required)
|
14
|
+
def acts_as_state_machine(options = {}, &block)
|
15
|
+
class_eval do
|
16
|
+
extend ClassMethods
|
17
|
+
include InstanceMethods
|
18
|
+
|
19
|
+
# make sure only does it once...
|
20
|
+
unless respond_to?(:state_machine_classes)
|
21
|
+
write_inheritable_attribute :state_machine_classes, {}
|
22
|
+
class_inheritable_reader :state_machine_classes
|
23
|
+
before_create :set_initial_state
|
24
|
+
after_create :run_initial_state_actions
|
25
|
+
end
|
26
|
+
|
27
|
+
name = options.delete(:name) || 'default'
|
28
|
+
name = name.to_sym
|
29
|
+
|
30
|
+
raise DuplicateStateMachine unless state_machine_classes[name].nil?
|
31
|
+
state_machine_classes[name] = SupportingClasses::StateMachineFactory.build(name, self, options, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
def state_machines
|
38
|
+
read_inheritable_attribute :state_machine_classes
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_in_state(state_machine_name, number, state, *args)
|
42
|
+
with_state_scope state_machine_name, state do
|
43
|
+
find(number, *args)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def count_in_state(state_machine_name, state, *args)
|
48
|
+
with_state_scope state_machine_name, state do
|
49
|
+
count(*args)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def calculate_in_state(state_machine_name, state, *args)
|
54
|
+
with_state_scope state_machine_name, state do
|
55
|
+
calculate(*args)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def with_state_scope(state_machine_name, state)
|
60
|
+
sm = state_machine_classes[state_machine_name.to_sym]
|
61
|
+
raise InvalidState unless sm.states.include?(state.to_sym)
|
62
|
+
|
63
|
+
with_scope :find => {:conditions => ["#{table_name}.#{sm.state_column} = ?", state.to_s]} do
|
64
|
+
yield if block_given?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module InstanceMethods
|
70
|
+
def state_machines
|
71
|
+
@state_machines ||= state_machine_classes.inject({}) {|sm, (name,klass) | sm[name] = klass.new(self); sm}
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_initial_state
|
75
|
+
state_machines.values.each(&:set_initial_state)
|
76
|
+
end
|
77
|
+
|
78
|
+
def run_initial_state_actions
|
79
|
+
state_machines.values.each(&:run_initial_state_actions)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Acts
|
2
|
+
module As
|
3
|
+
module Multiple
|
4
|
+
module StateMachines
|
5
|
+
class InvalidState < Exception #:nodoc:
|
6
|
+
end
|
7
|
+
|
8
|
+
class NoInitialState < Exception #:nodoc:
|
9
|
+
end
|
10
|
+
|
11
|
+
class DuplicateStateMachine < Exception #:nodoc:
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Acts
|
2
|
+
module As
|
3
|
+
module Multiple
|
4
|
+
module StateMachines
|
5
|
+
module SupportingClasses
|
6
|
+
class Event
|
7
|
+
|
8
|
+
attr_reader :name
|
9
|
+
attr_reader :transitions
|
10
|
+
attr_reader :opts
|
11
|
+
attr_reader :sm
|
12
|
+
|
13
|
+
def initialize(state_machine, name, opts, transition_table, &block)
|
14
|
+
@name = name.to_sym
|
15
|
+
@transitions = transition_table[@name] = []
|
16
|
+
@sm = state_machine
|
17
|
+
|
18
|
+
instance_eval(&block) if block
|
19
|
+
@opts = opts
|
20
|
+
@opts.freeze
|
21
|
+
@transitions.freeze
|
22
|
+
freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
def next_states(record)
|
26
|
+
@transitions.select { |t| t.from == sm.current_state(record).to_s }
|
27
|
+
end
|
28
|
+
|
29
|
+
def fire(record)
|
30
|
+
next_states(record).each do |transition|
|
31
|
+
break true if transition.perform(record)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def transitions(trans_opts)
|
36
|
+
Array(trans_opts[:from]).each do |s|
|
37
|
+
@transitions << SupportingClasses::StateTransition.new(sm, trans_opts.merge({:from => s.to_sym}))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Acts
|
2
|
+
module As
|
3
|
+
module Multiple
|
4
|
+
module StateMachines
|
5
|
+
module SupportingClasses
|
6
|
+
class State
|
7
|
+
|
8
|
+
attr_reader :name, :value, :sm
|
9
|
+
|
10
|
+
def initialize(state_machine, name, options)
|
11
|
+
@name = name.to_sym
|
12
|
+
@value = (options[:value] || @name).to_s
|
13
|
+
@after = Array(options[:after])
|
14
|
+
@enter = options[:enter] || NOOP
|
15
|
+
@exit = options[:exit] || NOOP
|
16
|
+
@sm = state_machine
|
17
|
+
end
|
18
|
+
|
19
|
+
def entering(record)
|
20
|
+
sm.send(:run_transition_action, record, @enter)
|
21
|
+
end
|
22
|
+
|
23
|
+
def entered(record)
|
24
|
+
@after.each { |action| sm.send(:run_transition_action, record, action) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def exited(record)
|
28
|
+
sm.send(:run_transition_action, record, @exit)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Acts
|
2
|
+
module As
|
3
|
+
module Multiple
|
4
|
+
module StateMachines
|
5
|
+
module SupportingClasses
|
6
|
+
class StateMachine
|
7
|
+
class_inheritable_reader :name,
|
8
|
+
:initial_state,
|
9
|
+
:state_column,
|
10
|
+
:state_map,
|
11
|
+
:transition_table,
|
12
|
+
:event_table
|
13
|
+
|
14
|
+
attr_reader :record
|
15
|
+
|
16
|
+
def initialize(record)
|
17
|
+
@record = record
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def states
|
22
|
+
state_map.keys.collect { |state| state.to_sym }
|
23
|
+
end
|
24
|
+
|
25
|
+
def current_state(record)
|
26
|
+
record.send(state_column).to_sym
|
27
|
+
end
|
28
|
+
|
29
|
+
def event(event, opts={}, &block)
|
30
|
+
e = SupportingClasses::Event.new(self, event, opts, transition_table, &block)
|
31
|
+
event_table[event.to_sym] = e
|
32
|
+
class_eval "
|
33
|
+
def #{event}!
|
34
|
+
event_table[:#{event}].fire(record)
|
35
|
+
end
|
36
|
+
"
|
37
|
+
end
|
38
|
+
|
39
|
+
def state(name, opts={})
|
40
|
+
state = SupportingClasses::State.new(self, name, opts)
|
41
|
+
state_map[state.value] = state
|
42
|
+
state_name = state.name
|
43
|
+
class_eval "
|
44
|
+
def #{state.name}?
|
45
|
+
current_state.to_s == state_map['#{state.value}'].value
|
46
|
+
end
|
47
|
+
"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def set_config_options(name, options)
|
52
|
+
write_inheritable_attribute :name, name
|
53
|
+
write_inheritable_attribute :state_map, {}
|
54
|
+
write_inheritable_attribute :initial_state, options[:initial]
|
55
|
+
write_inheritable_attribute :transition_table, {}
|
56
|
+
write_inheritable_attribute :event_table, {}
|
57
|
+
write_inheritable_attribute :state_column, options[:column] || 'state'
|
58
|
+
end
|
59
|
+
|
60
|
+
def freeze_config!
|
61
|
+
state_map.freeze
|
62
|
+
transition_table.freeze
|
63
|
+
event_table.freeze
|
64
|
+
end
|
65
|
+
|
66
|
+
def configure(name, options, &block)
|
67
|
+
set_config_options(name, options)
|
68
|
+
instance_eval(&block)
|
69
|
+
freeze_config!
|
70
|
+
end
|
71
|
+
|
72
|
+
def run_transition_action(record, action)
|
73
|
+
Symbol === action ? record.method(action).call : action.call(record)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def set_initial_state
|
78
|
+
record.write_attribute state_column, initial_state.to_s
|
79
|
+
end
|
80
|
+
|
81
|
+
def run_initial_state_actions
|
82
|
+
initial = state_map[initial_state.to_s]
|
83
|
+
initial.entering(record)
|
84
|
+
initial.entered(record)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns the current state the object is in, as a Ruby symbol.
|
88
|
+
def current_state
|
89
|
+
self.class.current_state(record)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns what the next state for a given event would be, as a Ruby symbol.
|
93
|
+
def next_state_for_event(event)
|
94
|
+
ns = next_states_for_event(event)
|
95
|
+
ns.empty? ? nil : ns.first.to.to_sym
|
96
|
+
end
|
97
|
+
|
98
|
+
def next_states_for_event(event)
|
99
|
+
transition_table[event.to_sym].select do |s|
|
100
|
+
s.from == current_state.to_s
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def states
|
105
|
+
self.class.states
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Acts
|
2
|
+
module As
|
3
|
+
module Multiple
|
4
|
+
module StateMachines
|
5
|
+
module SupportingClasses
|
6
|
+
class StateMachineFactory
|
7
|
+
class << self
|
8
|
+
def build(name, model, options, &block)
|
9
|
+
raise NoInitialState unless options[:initial]
|
10
|
+
klass = model.const_set(state_machine_class_name(name), Class.new(SupportingClasses::StateMachine))
|
11
|
+
klass.send :configure, name, options, &block
|
12
|
+
klass
|
13
|
+
end
|
14
|
+
|
15
|
+
def state_machine_class_name(name)
|
16
|
+
"#{name.to_s.classify}StateMachine"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Acts
|
2
|
+
module As
|
3
|
+
module Multiple
|
4
|
+
module StateMachines
|
5
|
+
module SupportingClasses
|
6
|
+
class StateTransition
|
7
|
+
|
8
|
+
attr_reader :from, :to, :opts, :sm
|
9
|
+
|
10
|
+
def initialize(state_machine, options)
|
11
|
+
@from = options[:from].to_s
|
12
|
+
@to = options[:to].to_s
|
13
|
+
@guard = options[:guard] || NOOP
|
14
|
+
@opts = options
|
15
|
+
@sm = state_machine
|
16
|
+
end
|
17
|
+
|
18
|
+
def guard(obj)
|
19
|
+
@guard ? sm.send(:run_transition_action, obj, @guard) : true
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform(record)
|
23
|
+
return false unless guard(record)
|
24
|
+
loopback = sm.current_state(record).to_s == to
|
25
|
+
states = sm.state_map
|
26
|
+
next_state = states[to]
|
27
|
+
old_state = states[sm.current_state(record).to_s]
|
28
|
+
|
29
|
+
next_state.entering(record) unless loopback
|
30
|
+
|
31
|
+
record.update_attribute(sm.state_column, next_state.value)
|
32
|
+
|
33
|
+
next_state.entered(record) unless loopback
|
34
|
+
old_state.exited(record) unless loopback
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def ==(obj)
|
39
|
+
@from == obj.from && @to == obj.to
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/acts_as_multiple_state_machines'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/exceptions'
|
3
|
+
require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes'
|
4
|
+
require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes/event'
|
5
|
+
require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes/state'
|
6
|
+
require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes/state_transition'
|
7
|
+
require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes/state_machine'
|
8
|
+
require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes/state_machine_factory'
|
9
|
+
require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/active_record_extension'
|
10
|
+
|
11
|
+
ActiveRecord::Base.class_eval do
|
12
|
+
include ::Acts::As::Multiple::StateMachines
|
13
|
+
end
|
@@ -0,0 +1,697 @@
|
|
1
|
+
RAILS_ROOT = File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "test/unit"
|
5
|
+
require "active_record"
|
6
|
+
require "active_record/fixtures"
|
7
|
+
|
8
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
9
|
+
require File.dirname(__FILE__) + "/../rails/init"
|
10
|
+
|
11
|
+
# Log everything to a global StringIO object instead of a file.
|
12
|
+
require "stringio"
|
13
|
+
$LOG = StringIO.new
|
14
|
+
$LOGGER = Logger.new($LOG)
|
15
|
+
ActiveRecord::Base.logger = $LOGGER
|
16
|
+
|
17
|
+
ActiveRecord::Base.configurations = {
|
18
|
+
"sqlite" => {
|
19
|
+
:adapter => "sqlite",
|
20
|
+
:dbfile => "state_machine.sqlite.db"
|
21
|
+
},
|
22
|
+
|
23
|
+
"sqlite3" => {
|
24
|
+
:adapter => "sqlite3",
|
25
|
+
:dbfile => ":memory:"
|
26
|
+
},
|
27
|
+
|
28
|
+
"mysql" => {
|
29
|
+
:adapter => "mysql",
|
30
|
+
:host => "localhost",
|
31
|
+
:username => "rails",
|
32
|
+
:password => nil,
|
33
|
+
:database => "state_machine_test"
|
34
|
+
},
|
35
|
+
|
36
|
+
"postgresql" => {
|
37
|
+
:min_messages => "ERROR",
|
38
|
+
:adapter => "postgresql",
|
39
|
+
:username => "postgres",
|
40
|
+
:password => "postgres",
|
41
|
+
:database => "state_machine_test"
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
# Connect to the database.
|
46
|
+
ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite3")
|
47
|
+
|
48
|
+
# Create table for conversations.
|
49
|
+
ActiveRecord::Migration.verbose = false
|
50
|
+
ActiveRecord::Schema.define(:version => 1) do
|
51
|
+
create_table :conversations, :force => true do |t|
|
52
|
+
t.column :state_machine, :string
|
53
|
+
t.column :subject, :string
|
54
|
+
t.column :closed, :boolean
|
55
|
+
t.column :another_state_machine, :string
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Test::Unit::TestCase
|
60
|
+
self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
|
61
|
+
self.use_transactional_fixtures = true
|
62
|
+
self.use_instantiated_fixtures = false
|
63
|
+
|
64
|
+
def create_fixtures(*table_names, &block)
|
65
|
+
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names, &block)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Conversation < ActiveRecord::Base
|
70
|
+
attr_writer :can_close
|
71
|
+
attr_accessor :read_enter, :read_exit,
|
72
|
+
:needs_attention_enter, :needs_attention_after,
|
73
|
+
:read_after_first, :read_after_second,
|
74
|
+
:closed_after
|
75
|
+
|
76
|
+
# How's THAT for self-documenting? ;-)
|
77
|
+
def always_true
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
def can_close?
|
82
|
+
!!@can_close
|
83
|
+
end
|
84
|
+
|
85
|
+
def read_enter_action
|
86
|
+
self.read_enter = true
|
87
|
+
end
|
88
|
+
|
89
|
+
def read_after_first_action
|
90
|
+
self.read_after_first = true
|
91
|
+
end
|
92
|
+
|
93
|
+
def read_after_second_action
|
94
|
+
self.read_after_second = true
|
95
|
+
end
|
96
|
+
|
97
|
+
def closed_after_action
|
98
|
+
self.closed_after = true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class ActsAsStateMachineTest < Test::Unit::TestCase
|
103
|
+
include ::Acts::As::Multiple::StateMachines
|
104
|
+
fixtures :conversations
|
105
|
+
|
106
|
+
def teardown
|
107
|
+
Conversation.class_eval do
|
108
|
+
state_machine_classes.keys.each do |name|
|
109
|
+
remove_const SupportingClasses::StateMachineFactory.state_machine_class_name(name)
|
110
|
+
end
|
111
|
+
|
112
|
+
write_inheritable_attribute :state_machine_classes, {}
|
113
|
+
|
114
|
+
# Clear out any callbacks that were set by acts_as_state_machine.
|
115
|
+
write_inheritable_attribute :before_create, []
|
116
|
+
write_inheritable_attribute :after_create, []
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_no_initial_value_raises_exception
|
121
|
+
assert_raises(NoInitialState) do
|
122
|
+
Conversation.class_eval { acts_as_state_machine }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_state_column
|
127
|
+
Conversation.class_eval do
|
128
|
+
acts_as_state_machine(:initial => :needs_attention, :column => "state_machine") do
|
129
|
+
state :needs_attention
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
assert_equal "state_machine", Conversation.state_machines[:default].state_column
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_initial_state_value
|
137
|
+
Conversation.class_eval do
|
138
|
+
acts_as_state_machine :initial => :needs_attention do
|
139
|
+
state :needs_attention
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
assert_equal :needs_attention, Conversation.state_machines[:default].initial_state
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_initial_state
|
147
|
+
Conversation.class_eval do
|
148
|
+
acts_as_state_machine :initial => :needs_attention do
|
149
|
+
state :needs_attention
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
c = Conversation.create!
|
154
|
+
assert_equal :needs_attention, c.state_machines[:default].current_state
|
155
|
+
assert c.state_machines[:default].needs_attention?
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_states_were_set
|
159
|
+
Conversation.class_eval do
|
160
|
+
acts_as_state_machine :initial => :needs_attention do
|
161
|
+
state :needs_attention
|
162
|
+
state :read
|
163
|
+
state :closed
|
164
|
+
state :awaiting_response
|
165
|
+
state :junk
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
[:needs_attention, :read, :closed, :awaiting_response, :junk].each do |state|
|
170
|
+
assert Conversation.state_machines[:default].states.include?(state)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_query_methods_created
|
175
|
+
Conversation.class_eval do
|
176
|
+
acts_as_state_machine :initial => :needs_attention do
|
177
|
+
state :needs_attention
|
178
|
+
state :read
|
179
|
+
state :closed
|
180
|
+
state :awaiting_response
|
181
|
+
state :junk
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
c = Conversation.create!
|
186
|
+
[:needs_attention?, :read?, :closed?, :awaiting_response?, :junk?].each do |query|
|
187
|
+
assert c.state_machines[:default].respond_to?(query)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def test_event_methods_created
|
192
|
+
Conversation.class_eval do
|
193
|
+
acts_as_state_machine :initial => :needs_attention do
|
194
|
+
state :needs_attention
|
195
|
+
state :read
|
196
|
+
state :closed
|
197
|
+
state :awaiting_response
|
198
|
+
state :junk
|
199
|
+
|
200
|
+
event(:new_message) {}
|
201
|
+
event(:view) {}
|
202
|
+
event(:reply) {}
|
203
|
+
event(:close) {}
|
204
|
+
event(:junk, :note => "finished") {}
|
205
|
+
event(:unjunk) {}
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
c = Conversation.create!
|
210
|
+
[:new_message!, :view!, :reply!, :close!, :junk!, :unjunk!].each do |event|
|
211
|
+
assert c.state_machines[:default].respond_to?(event)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_transition_table
|
216
|
+
Conversation.class_eval do
|
217
|
+
acts_as_state_machine :initial => :needs_attention do
|
218
|
+
state :needs_attention
|
219
|
+
state :read
|
220
|
+
state :closed
|
221
|
+
state :awaiting_response
|
222
|
+
state :junk
|
223
|
+
|
224
|
+
event :new_message do
|
225
|
+
transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
sm = Conversation.state_machines[:default]
|
231
|
+
tt = sm.transition_table
|
232
|
+
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(sm, :from => :read, :to => :needs_attention))
|
233
|
+
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(sm, :from => :closed, :to => :needs_attention))
|
234
|
+
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(sm, :from => :awaiting_response, :to => :needs_attention))
|
235
|
+
end
|
236
|
+
|
237
|
+
def test_next_state_for_event
|
238
|
+
Conversation.class_eval do
|
239
|
+
acts_as_state_machine :initial => :needs_attention do
|
240
|
+
state :needs_attention
|
241
|
+
state :read
|
242
|
+
|
243
|
+
event :view do
|
244
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
c = Conversation.create!
|
250
|
+
assert_equal :read, c.state_machines[:default].next_state_for_event(:view)
|
251
|
+
end
|
252
|
+
|
253
|
+
def test_change_state
|
254
|
+
Conversation.class_eval do
|
255
|
+
acts_as_state_machine :initial => :needs_attention do
|
256
|
+
state :needs_attention
|
257
|
+
state :read
|
258
|
+
|
259
|
+
event :view do
|
260
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
c = Conversation.create!
|
266
|
+
c.state_machines[:default].view!
|
267
|
+
assert c.state_machines[:default].read?
|
268
|
+
end
|
269
|
+
|
270
|
+
def test_can_go_from_read_to_closed_because_guard_passes
|
271
|
+
Conversation.class_eval do
|
272
|
+
acts_as_state_machine :initial => :needs_attention do
|
273
|
+
state :needs_attention
|
274
|
+
state :read
|
275
|
+
state :closed
|
276
|
+
state :awaiting_response
|
277
|
+
|
278
|
+
event :view do
|
279
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
280
|
+
end
|
281
|
+
|
282
|
+
event :reply do
|
283
|
+
transitions :to => :awaiting_response, :from => [:read, :closed]
|
284
|
+
end
|
285
|
+
|
286
|
+
event :close do
|
287
|
+
transitions :to => :closed, :from => [:read, :awaiting_response], :guard => lambda { |o| o.can_close? }
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
c = Conversation.create!
|
293
|
+
c.can_close = true
|
294
|
+
c.state_machines[:default].view!
|
295
|
+
c.state_machines[:default].reply!
|
296
|
+
c.state_machines[:default].close!
|
297
|
+
assert_equal :closed, c.state_machines[:default].current_state
|
298
|
+
end
|
299
|
+
|
300
|
+
def test_cannot_go_from_read_to_closed_because_of_guard
|
301
|
+
Conversation.class_eval do
|
302
|
+
acts_as_state_machine :initial => :needs_attention do
|
303
|
+
state :needs_attention
|
304
|
+
state :read
|
305
|
+
state :closed
|
306
|
+
state :awaiting_response
|
307
|
+
|
308
|
+
event :view do
|
309
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
310
|
+
end
|
311
|
+
|
312
|
+
event :reply do
|
313
|
+
transitions :to => :awaiting_response, :from => [:read, :closed]
|
314
|
+
end
|
315
|
+
|
316
|
+
event :close do
|
317
|
+
transitions :to => :closed, :from => [:read, :awaiting_response], :guard => lambda { |o| o.can_close? }
|
318
|
+
transitions :to => :read, :from => [:read, :awaiting_response], :guard => :always_true
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
c = Conversation.create!
|
324
|
+
c.can_close = false
|
325
|
+
c.state_machines[:default].view!
|
326
|
+
c.state_machines[:default].reply!
|
327
|
+
c.state_machines[:default].close!
|
328
|
+
assert_equal :read, c.state_machines[:default].current_state
|
329
|
+
end
|
330
|
+
|
331
|
+
def test_ignore_invalid_events
|
332
|
+
Conversation.class_eval do
|
333
|
+
acts_as_state_machine :initial => :needs_attention do
|
334
|
+
state :needs_attention
|
335
|
+
state :read
|
336
|
+
state :closed
|
337
|
+
state :awaiting_response
|
338
|
+
state :junk
|
339
|
+
|
340
|
+
event :new_message do
|
341
|
+
transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
|
342
|
+
end
|
343
|
+
|
344
|
+
event :view do
|
345
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
346
|
+
end
|
347
|
+
|
348
|
+
event :junk, :note => "finished" do
|
349
|
+
transitions :to => :junk, :from => [:read, :closed, :awaiting_response]
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
c = Conversation.create
|
355
|
+
c.state_machines[:default].view!
|
356
|
+
c.state_machines[:default].junk!
|
357
|
+
|
358
|
+
# This is the invalid event
|
359
|
+
c.state_machines[:default].new_message!
|
360
|
+
assert_equal :junk, c.state_machines[:default].current_state
|
361
|
+
end
|
362
|
+
|
363
|
+
def test_entry_action_executed
|
364
|
+
Conversation.class_eval do
|
365
|
+
acts_as_state_machine :initial => :needs_attention do
|
366
|
+
state :needs_attention
|
367
|
+
state :read, :enter => :read_enter_action
|
368
|
+
|
369
|
+
event :view do
|
370
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
c = Conversation.create!
|
376
|
+
c.read_enter = false
|
377
|
+
c.state_machines[:default].view!
|
378
|
+
assert c.read_enter
|
379
|
+
end
|
380
|
+
|
381
|
+
def test_after_actions_executed
|
382
|
+
Conversation.class_eval do
|
383
|
+
acts_as_state_machine :initial => :needs_attention do
|
384
|
+
state :needs_attention
|
385
|
+
state :closed, :after => :closed_after_action
|
386
|
+
state :read, :enter => :read_enter_action,
|
387
|
+
:exit => Proc.new { |o| o.read_exit = true },
|
388
|
+
:after => [:read_after_first_action, :read_after_second_action]
|
389
|
+
|
390
|
+
event :view do
|
391
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
392
|
+
end
|
393
|
+
|
394
|
+
event :close do
|
395
|
+
transitions :to => :closed, :from => [:read, :awaiting_response]
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
c = Conversation.create!
|
401
|
+
|
402
|
+
c.read_after_first = false
|
403
|
+
c.read_after_second = false
|
404
|
+
c.closed_after = false
|
405
|
+
|
406
|
+
c.state_machines[:default].view!
|
407
|
+
assert c.read_after_first
|
408
|
+
assert c.read_after_second
|
409
|
+
|
410
|
+
c.can_close = true
|
411
|
+
c.state_machines[:default].close!
|
412
|
+
|
413
|
+
assert c.closed_after
|
414
|
+
assert_equal :closed, c.state_machines[:default].current_state
|
415
|
+
end
|
416
|
+
|
417
|
+
def test_after_actions_not_run_on_loopback_transition
|
418
|
+
Conversation.class_eval do
|
419
|
+
acts_as_state_machine :initial => :needs_attention do
|
420
|
+
state :needs_attention
|
421
|
+
state :closed, :after => :closed_after_action
|
422
|
+
state :read, :after => [:read_after_first_action, :read_after_second_action]
|
423
|
+
|
424
|
+
event :view do
|
425
|
+
transitions :to => :read, :from => :needs_attention
|
426
|
+
end
|
427
|
+
|
428
|
+
event :close do
|
429
|
+
transitions :to => :closed, :from => :read
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
c = Conversation.create!
|
435
|
+
|
436
|
+
c.state_machines[:default].view!
|
437
|
+
c.read_after_first = false
|
438
|
+
c.read_after_second = false
|
439
|
+
c.state_machines[:default].view!
|
440
|
+
|
441
|
+
assert !c.read_after_first
|
442
|
+
assert !c.read_after_second
|
443
|
+
|
444
|
+
c.can_close = true
|
445
|
+
|
446
|
+
c.state_machines[:default].close!
|
447
|
+
c.closed_after = false
|
448
|
+
c.state_machines[:default].close!
|
449
|
+
|
450
|
+
assert !c.closed_after
|
451
|
+
end
|
452
|
+
|
453
|
+
def test_exit_action_executed
|
454
|
+
Conversation.class_eval do
|
455
|
+
acts_as_state_machine :initial => :needs_attention do
|
456
|
+
state :junk
|
457
|
+
state :needs_attention
|
458
|
+
state :read, :exit => lambda { |o| o.read_exit = true }
|
459
|
+
|
460
|
+
event :view do
|
461
|
+
transitions :to => :read, :from => :needs_attention
|
462
|
+
end
|
463
|
+
|
464
|
+
event :junk, :note => "finished" do
|
465
|
+
transitions :to => :junk, :from => :read
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
c = Conversation.create!
|
471
|
+
c.read_exit = false
|
472
|
+
c.state_machines[:default].view!
|
473
|
+
c.state_machines[:default].junk!
|
474
|
+
assert c.read_exit
|
475
|
+
end
|
476
|
+
|
477
|
+
def test_entry_and_exit_not_run_on_loopback_transition
|
478
|
+
Conversation.class_eval do
|
479
|
+
acts_as_state_machine :initial => :needs_attention do
|
480
|
+
state :needs_attention
|
481
|
+
state :read, :exit => lambda { |o| o.read_exit = true }
|
482
|
+
|
483
|
+
event :view do
|
484
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
c = Conversation.create!
|
490
|
+
c.state_machines[:default].view!
|
491
|
+
c.read_enter = false
|
492
|
+
c.read_exit = false
|
493
|
+
c.state_machines[:default].view!
|
494
|
+
assert !c.read_enter
|
495
|
+
assert !c.read_exit
|
496
|
+
end
|
497
|
+
|
498
|
+
def test_entry_and_after_actions_called_for_initial_state
|
499
|
+
Conversation.class_eval do
|
500
|
+
acts_as_state_machine :initial => :needs_attention do
|
501
|
+
state :needs_attention, :enter => lambda { |o| o.needs_attention_enter = true },
|
502
|
+
:after => lambda { |o| o.needs_attention_after = true }
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
c = Conversation.create!
|
507
|
+
assert c.needs_attention_enter
|
508
|
+
assert c.needs_attention_after
|
509
|
+
end
|
510
|
+
|
511
|
+
def test_run_transition_action_is_private
|
512
|
+
Conversation.class_eval do
|
513
|
+
acts_as_state_machine :initial => :needs_attention do
|
514
|
+
state :needs_attention
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
c = Conversation.create!
|
519
|
+
assert_raises(NoMethodError) { c.state_machines[:default].run_transition_action :foo }
|
520
|
+
end
|
521
|
+
|
522
|
+
def test_find_all_in_state
|
523
|
+
Conversation.class_eval do
|
524
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
|
525
|
+
state :needs_attention
|
526
|
+
state :read
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
cs = Conversation.find_in_state(:default, :all, :read)
|
531
|
+
assert_equal 2, cs.size
|
532
|
+
end
|
533
|
+
|
534
|
+
def test_find_first_in_state
|
535
|
+
Conversation.class_eval do
|
536
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
|
537
|
+
state :needs_attention
|
538
|
+
state :read
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
c = Conversation.find_in_state(:default, :first, :read)
|
543
|
+
assert_equal conversations(:first).id, c.id
|
544
|
+
end
|
545
|
+
|
546
|
+
def test_find_all_in_state_with_conditions
|
547
|
+
Conversation.class_eval do
|
548
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
|
549
|
+
state :needs_attention
|
550
|
+
state :read
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
cs = Conversation.find_in_state(:default, :all, :read, :conditions => ['subject = ?', conversations(:second).subject])
|
555
|
+
|
556
|
+
assert_equal 1, cs.size
|
557
|
+
assert_equal conversations(:second).id, cs.first.id
|
558
|
+
end
|
559
|
+
|
560
|
+
def test_find_first_in_state_with_conditions
|
561
|
+
Conversation.class_eval do
|
562
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
|
563
|
+
state :needs_attention
|
564
|
+
state :read
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
c = Conversation.find_in_state(:default, :first, :read, :conditions => ['subject = ?', conversations(:second).subject])
|
569
|
+
assert_equal conversations(:second).id, c.id
|
570
|
+
end
|
571
|
+
|
572
|
+
def test_count_in_state
|
573
|
+
Conversation.class_eval do
|
574
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
|
575
|
+
state :needs_attention
|
576
|
+
state :read
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
cnt0 = Conversation.count(:conditions => ['state_machine = ?', 'read'])
|
581
|
+
cnt = Conversation.count_in_state(:default, :read)
|
582
|
+
|
583
|
+
assert_equal cnt0, cnt
|
584
|
+
end
|
585
|
+
|
586
|
+
def test_count_in_state_with_conditions
|
587
|
+
Conversation.class_eval do
|
588
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
|
589
|
+
state :needs_attention
|
590
|
+
state :read
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
cnt0 = Conversation.count(:conditions => ['state_machine = ? AND subject = ?', 'read', 'Foo'])
|
595
|
+
cnt = Conversation.count_in_state(:default, :read, :conditions => ['subject = ?', 'Foo'])
|
596
|
+
|
597
|
+
assert_equal cnt0, cnt
|
598
|
+
end
|
599
|
+
|
600
|
+
def test_find_in_invalid_state_raises_exception
|
601
|
+
Conversation.class_eval do
|
602
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
|
603
|
+
state :needs_attention
|
604
|
+
state :read
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
assert_raises(InvalidState) do
|
609
|
+
Conversation.find_in_state(:default, :all, :dead)
|
610
|
+
end
|
611
|
+
end
|
612
|
+
|
613
|
+
def test_count_in_invalid_state_raises_exception
|
614
|
+
Conversation.class_eval do
|
615
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
|
616
|
+
state :needs_attention
|
617
|
+
state :read
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
assert_raise(InvalidState) do
|
622
|
+
Conversation.count_in_state(:default, :dead)
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
def test_can_access_events_via_event_table
|
627
|
+
Conversation.class_eval do
|
628
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
|
629
|
+
state :needs_attention
|
630
|
+
state :junk
|
631
|
+
|
632
|
+
event :junk, :note => "finished" do
|
633
|
+
transitions :to => :junk, :from => :needs_attention
|
634
|
+
end
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
event = Conversation.state_machines[:default].event_table[:junk]
|
639
|
+
assert_equal :junk, event.name
|
640
|
+
assert_equal "finished", event.opts[:note]
|
641
|
+
end
|
642
|
+
|
643
|
+
def test_custom_state_values
|
644
|
+
Conversation.class_eval do
|
645
|
+
acts_as_state_machine :initial => "NEEDS_ATTENTION", :column => "state_machine" do
|
646
|
+
state :needs_attention, :value => "NEEDS_ATTENTION"
|
647
|
+
state :read, :value => "READ"
|
648
|
+
|
649
|
+
event :view do
|
650
|
+
transitions :to => "READ", :from => ["NEEDS_ATTENTION", "READ"]
|
651
|
+
end
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
c = Conversation.create!
|
656
|
+
assert_equal "NEEDS_ATTENTION", c.state_machine
|
657
|
+
assert c.state_machines[:default].needs_attention?
|
658
|
+
c.state_machines[:default].view!
|
659
|
+
assert_equal "READ", c.state_machine
|
660
|
+
assert c.state_machines[:default].read?
|
661
|
+
end
|
662
|
+
|
663
|
+
|
664
|
+
def test_can_define_multiple_state_machines
|
665
|
+
Conversation.class_eval do
|
666
|
+
acts_as_state_machine :name => 'default', :initial => :needs_attention, :column => "state_machine" do
|
667
|
+
state :needs_attention
|
668
|
+
state :junk
|
669
|
+
|
670
|
+
event :junk do
|
671
|
+
transitions :to => :junk, :from => :needs_attention
|
672
|
+
end
|
673
|
+
end
|
674
|
+
|
675
|
+
acts_as_state_machine :name => 'user', :initial => :pending, :column => "another_state_machine" do
|
676
|
+
state :pending
|
677
|
+
state :active
|
678
|
+
|
679
|
+
event :activate do
|
680
|
+
transitions :to => :active, :from => :pending
|
681
|
+
end
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
c = Conversation.create!
|
686
|
+
assert_equal 'needs_attention', c.state_machine
|
687
|
+
assert_equal 'pending', c.another_state_machine
|
688
|
+
assert c.state_machines[:default].needs_attention?
|
689
|
+
assert c.state_machines[:user].pending?
|
690
|
+
c.state_machines[:default].junk!
|
691
|
+
assert c.state_machines[:default].junk?
|
692
|
+
assert c.state_machines[:user].pending?
|
693
|
+
c.state_machines[:user].activate!
|
694
|
+
assert c.state_machines[:default].junk?
|
695
|
+
assert c.state_machines[:user].active?
|
696
|
+
end
|
697
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aq1018-acts_as_multiple_state_machines
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aaron Qian
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-08 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: similar to acts_as_state_machine, but provides multiple State Machines per ActiveRecord Model
|
17
|
+
email: aaron [a] ekohe [d] com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- History.txt
|
24
|
+
- Manifest.txt
|
25
|
+
- README.txt
|
26
|
+
files:
|
27
|
+
- History.txt
|
28
|
+
- Manifest.txt
|
29
|
+
- README.txt
|
30
|
+
- Rakefile
|
31
|
+
- rails/init.rb
|
32
|
+
- lib/acts_as_multiple_state_machines.rb
|
33
|
+
- lib/acts/as/multiple/state_machines/active_record_extension.rb
|
34
|
+
- lib/acts/as/multiple/state_machines/exceptions.rb
|
35
|
+
- lib/acts/as/multiple/state_machines/supporting_classes.rb
|
36
|
+
- lib/acts/as/multiple/state_machines/supporting_classes/event.rb
|
37
|
+
- lib/acts/as/multiple/state_machines/supporting_classes/state.rb
|
38
|
+
- lib/acts/as/multiple/state_machines/supporting_classes/state_machine.rb
|
39
|
+
- lib/acts/as/multiple/state_machines/supporting_classes/state_machine_factory.rb
|
40
|
+
- lib/acts/as/multiple/state_machines/supporting_classes/state_transition.rb
|
41
|
+
- test/test_acts_as_multiple_state_machines.rb
|
42
|
+
- test/fixtures/conversations.yml
|
43
|
+
has_rdoc: true
|
44
|
+
homepage: http://acts-as-multiple-state-machines.rubyforge.com
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options:
|
47
|
+
- --main
|
48
|
+
- README.txt
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: "0"
|
62
|
+
version:
|
63
|
+
requirements: []
|
64
|
+
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.2.0
|
67
|
+
signing_key:
|
68
|
+
specification_version: 2
|
69
|
+
summary: similar to acts_as_state_machine, but provides multiple State Machines per ActiveRecord Model
|
70
|
+
test_files:
|
71
|
+
- test/fixtures/conversations.yml
|
72
|
+
- test/test_acts_as_multiple_state_machines.rb
|