can_has_state 0.2.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.
- data/MIT-LICENSE +20 -0
- data/README.md +238 -0
- data/Rakefile +38 -0
- data/lib/can_has_state/definition.rb +125 -0
- data/lib/can_has_state/machine.rb +104 -0
- data/lib/can_has_state/railtie.rb +11 -0
- data/lib/can_has_state/version.rb +3 -0
- data/lib/can_has_state.rb +7 -0
- data/lib/tasks/can_has_state_tasks.rake +4 -0
- data/test/can_has_state_test.rb +7 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config/application.rb +59 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/test_helper.rb +15 -0
- metadata +169 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 thomas morgan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
# CanHasState #
|
2
|
+
|
3
|
+
|
4
|
+
`can_has_state` is a simplified state machine gem. It relies on ActiveModel and
|
5
|
+
should be compatible with any ActiveModel-compatible persistence layer.
|
6
|
+
|
7
|
+
Key features:
|
8
|
+
|
9
|
+
* Support for multiple state machines
|
10
|
+
* Simplified DSL syntax
|
11
|
+
* Few added methods avoids clobbering up model's namespace
|
12
|
+
* Compatible with ActionPack-style attribute value changes via
|
13
|
+
`:state_column => 'new_state'`
|
14
|
+
* Use any ActiveModel-compatible persistence layer (including your own)
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
## DSL ##
|
19
|
+
|
20
|
+
### ActiveRecord ###
|
21
|
+
|
22
|
+
class Account < ActiveRecord::Base
|
23
|
+
|
24
|
+
# Choose your state column name. In this case, it's :state.
|
25
|
+
# It's super easy to have multiple state machines.
|
26
|
+
#
|
27
|
+
state_machine :state do
|
28
|
+
|
29
|
+
# Define each possible state. Add :initial to indicate which state
|
30
|
+
# will be selected first, if the state hasn't been already set. If
|
31
|
+
# not provided, will set :initial to the first defined state.
|
32
|
+
#
|
33
|
+
# :on_enter and :on_exit trigger when this state is entered / exited.
|
34
|
+
# Symbols are assumed to be instance method names. Inline blocks may
|
35
|
+
# also be specified. The _deferred variations are discussed below
|
36
|
+
# under triggers.
|
37
|
+
#
|
38
|
+
state :active, :initial,
|
39
|
+
:from => :inactive,
|
40
|
+
:on_enter => :update_plan_details,
|
41
|
+
:on_enter_deferred => :start_billing,
|
42
|
+
:on_exit_deferred => lambda { |r| r.stop_billing }
|
43
|
+
|
44
|
+
# :from restricts which states can switch to this one. Multiple "from"
|
45
|
+
# states are allowed, as shown under state :deleted.
|
46
|
+
#
|
47
|
+
# If :from is not present, this state may be entered from any other
|
48
|
+
# state. To prevent ever moving to a given state (only useful if that
|
49
|
+
# state is also the initial state), use :from => [] .
|
50
|
+
#
|
51
|
+
state :inactive,
|
52
|
+
:from => [:active]
|
53
|
+
|
54
|
+
# :timestamp automatically sets the current date/time when this state
|
55
|
+
# is entered. Both *_at and *_on (datetime and date) columns are
|
56
|
+
# supported.
|
57
|
+
#
|
58
|
+
# :require adds additional restrictions before this state can be
|
59
|
+
# entered. Like :on_enter/:on_exit, it can be a method name or a
|
60
|
+
# lambda/Proc. Multiple methods may be provided. Each method must
|
61
|
+
# return a ruby true value (anything except nil or false) for the
|
62
|
+
# condition to be satisfied. If any :require is not true, then the
|
63
|
+
# state transition is blocked.
|
64
|
+
#
|
65
|
+
# :message allows the validation error message to be customized. It is
|
66
|
+
# used when conditions for either :from or :require fail. The default
|
67
|
+
# message is used in the example below. %{from} and %{to} parameters
|
68
|
+
# are optional, and will be the from and to states, respectively.
|
69
|
+
#
|
70
|
+
state :deleted,
|
71
|
+
:from => [:active, :inactive],
|
72
|
+
:timestamp => :deleted_at,
|
73
|
+
:on_enter_deferred => [:delete_record, :delete_payment_info],
|
74
|
+
:require => lambda {|r| !r.active_services? },
|
75
|
+
:message => "has invalid transition from %{from} to %{to}"
|
76
|
+
|
77
|
+
# Custom triggers are called for certain "from" => "to" state
|
78
|
+
# combinations. They are especially useful for DRYing up triggers
|
79
|
+
# that apply in multiple situations. They can also be used to apply
|
80
|
+
# triggers only in narrower situations than :on_enter/:on_exit above.
|
81
|
+
|
82
|
+
# This triggers *only* on :inactive => :active. It will not trigger on
|
83
|
+
# nil => :active (setting initial state) [see notes on triggers and
|
84
|
+
# initial state below].
|
85
|
+
#
|
86
|
+
on :inactive => :active, :trigger => :send_welcome_back_message
|
87
|
+
|
88
|
+
# Multiple triggers can be specified on either side of the transition
|
89
|
+
# or for the trigger actions:
|
90
|
+
#
|
91
|
+
on [:active, :inactive] => :deleted, :trigger => [:do_one, :do_two]
|
92
|
+
on :active => [:inactive, :deleted], :trigger => lambda {|r| r.act }
|
93
|
+
|
94
|
+
# If :deferred is included, and it's true, then this trigger will
|
95
|
+
# happen post-save, instead of pre-validation. Default pre-validation
|
96
|
+
# triggers are recommended for changing other attributes. Post-save
|
97
|
+
# triggers are useful for logging or cascading changes to association
|
98
|
+
# models. Deferred trigger actions are run within the same database
|
99
|
+
# transaction (for ActiveRecord and other ActiveModel children that
|
100
|
+
# implement this).
|
101
|
+
#
|
102
|
+
# Last, wildcards are supported. Note that the ruby parser requires a
|
103
|
+
# space after the asterisk for wildcards on the left side:
|
104
|
+
# works: :* =>:whatever
|
105
|
+
# doesn't: :*=>:whatever
|
106
|
+
#
|
107
|
+
# Utilize ActiveModel::Dirty's change history support to know what has
|
108
|
+
# changed:
|
109
|
+
# from = state_was # (specifically, <state_column>_was)
|
110
|
+
# to = state
|
111
|
+
#
|
112
|
+
on :* => :*, :trigger => :log_state_change, :deferred=>true
|
113
|
+
on :* => :deleted, :trigger => :same_as_on_enter
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
# If you want to set states via ActionController/ActionView, you'll
|
118
|
+
# probably want something like this.
|
119
|
+
attr_accessible :state, :as=>:admin
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
### Just ActiveModel ###
|
125
|
+
|
126
|
+
class Account
|
127
|
+
include ActiveModel::Dirty
|
128
|
+
include ActiveModel::Validations
|
129
|
+
include ActiveModel::Validations::Callbacks
|
130
|
+
include CanHasState::Machine
|
131
|
+
|
132
|
+
state_machine :account_state do
|
133
|
+
state :active, :initial,
|
134
|
+
:from => :inactive
|
135
|
+
state :inactive,
|
136
|
+
:from => :active
|
137
|
+
state :deleted,
|
138
|
+
:from => [:active, :inactive],
|
139
|
+
:timestamp => :deleted_at
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
|
146
|
+
## Managing states ##
|
147
|
+
|
148
|
+
States are set directly via the relevant state column--no added methods.
|
149
|
+
|
150
|
+
@account = Account.new
|
151
|
+
@account.save!
|
152
|
+
@account.state
|
153
|
+
# => 'active'
|
154
|
+
|
155
|
+
@account.state = 'deleted'
|
156
|
+
@account.valid?
|
157
|
+
# => true
|
158
|
+
@account.save!
|
159
|
+
|
160
|
+
@account.state = 'active'
|
161
|
+
@account.valid?
|
162
|
+
# => false
|
163
|
+
|
164
|
+
|
165
|
+
With multiple state machines on a single model, this also eliminates any name
|
166
|
+
collisions.
|
167
|
+
|
168
|
+
class Account < ActiveRecord::Base
|
169
|
+
|
170
|
+
# column is :state
|
171
|
+
state_machine :state do
|
172
|
+
state :active,
|
173
|
+
:from => :inactive,
|
174
|
+
:require => lambda{|r| r.payment_status=='current'}
|
175
|
+
state :inactive,
|
176
|
+
:from => :active
|
177
|
+
state :deleted,
|
178
|
+
:from => [:active, :inactive]
|
179
|
+
end
|
180
|
+
|
181
|
+
# column is :payment_status
|
182
|
+
state_machine :payment_status do
|
183
|
+
state :pending, :initial
|
184
|
+
state :current
|
185
|
+
state :overdue
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
@account = Account.new
|
190
|
+
@account.save!
|
191
|
+
@account.state
|
192
|
+
# => 'active'
|
193
|
+
@account.payment_status
|
194
|
+
# => 'pending'
|
195
|
+
|
196
|
+
@account.state = 'inactive'
|
197
|
+
@account.payment_status = 'overdue'
|
198
|
+
@account.save!
|
199
|
+
|
200
|
+
|
201
|
+
You can also check a potential state change using the method
|
202
|
+
`"allow_#{state_machine_column_name}?(potential_state)"`.
|
203
|
+
|
204
|
+
@account.allow_state? :active
|
205
|
+
# => false
|
206
|
+
@account.allow_payment_status? :pending
|
207
|
+
# => true
|
208
|
+
|
209
|
+
|
210
|
+
If you really want event-changing methods, it's just as straight-forward to
|
211
|
+
write them as normal methods instead of attempting to cram them into a DSL.
|
212
|
+
|
213
|
+
def delete!
|
214
|
+
self.state = 'deleted'
|
215
|
+
save!
|
216
|
+
end
|
217
|
+
|
218
|
+
When `save!` is called, the state changes will be validated and all triggers
|
219
|
+
will be called.
|
220
|
+
|
221
|
+
|
222
|
+
|
223
|
+
## Notes on triggers and initial state ##
|
224
|
+
|
225
|
+
`can_has_state` relies on the `ActiveModel::Dirty` module to detect when a state
|
226
|
+
attribute has changed. In general, this shouldn't matter much to you.
|
227
|
+
|
228
|
+
However, triggers involving initial values can be tricky. If your database
|
229
|
+
schema sets the default value to the initial value, `:on_enter` and custom
|
230
|
+
triggers will *not* be called because nothing has changed. On the other hand,
|
231
|
+
if the state column defaults to a null value, then the triggers will be called
|
232
|
+
because the initial state value changed from nil to the initial state.
|
233
|
+
|
234
|
+
|
235
|
+
|
236
|
+
## Compatibility ##
|
237
|
+
|
238
|
+
Tested with Ruby 1.9 and ActiveSupport and ActiveModel 3.2.8.
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'CanHasState'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
Bundler::GemHelper.install_tasks
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
|
30
|
+
Rake::TestTask.new(:test) do |t|
|
31
|
+
t.libs << 'lib'
|
32
|
+
t.libs << 'test'
|
33
|
+
t.pattern = 'test/**/*_test.rb'
|
34
|
+
t.verbose = false
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
task :default => :test
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module CanHasState
|
2
|
+
class Definition
|
3
|
+
|
4
|
+
attr_reader :column, :states, :initial_state, :triggers
|
5
|
+
|
6
|
+
def initialize(column_name, &block)
|
7
|
+
@column = column_name.to_sym
|
8
|
+
@states = {}
|
9
|
+
@triggers = []
|
10
|
+
instance_eval &block
|
11
|
+
@initial_state ||= @states.keys.first
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def extend_machine(&block)
|
16
|
+
instance_eval &block
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def state(state_name, *args)
|
21
|
+
options = args.extract_options!
|
22
|
+
state_name = state_name.to_s
|
23
|
+
|
24
|
+
if args.include? :initial
|
25
|
+
@initial_state = state_name
|
26
|
+
end
|
27
|
+
|
28
|
+
# TODO: turn even guards into types of triggers ... then support :guard as a trigger param
|
29
|
+
guards = []
|
30
|
+
message = "has invalid transition from %{from} to %{to}"
|
31
|
+
# TODO: differentiate messages for :from errors vs. :guard errors
|
32
|
+
|
33
|
+
options.each do |key, val|
|
34
|
+
case key
|
35
|
+
when :from
|
36
|
+
from_vals = Array(val).map(&:to_s)
|
37
|
+
from_vals << nil # for new records
|
38
|
+
guards << Proc.new{|r| from_vals.include? r.send("#{column}_was").try(:to_s)}
|
39
|
+
when :guard, :require
|
40
|
+
guards += Array(val)
|
41
|
+
when :message
|
42
|
+
message = val
|
43
|
+
when :timestamp
|
44
|
+
@triggers << {:from=>["*"], :to=>[state_name], :trigger=>[Proc.new{|r| r.send("#{val}=", Time.current)}]}
|
45
|
+
when :on_enter
|
46
|
+
@triggers << {:from=>["*"], :to=>[state_name], :trigger=>Array(val), :type=>:on_enter}
|
47
|
+
when :on_enter_deferred
|
48
|
+
@triggers << {:from=>["*"], :to=>[state_name], :trigger=>Array(val), :type=>:on_enter, :deferred=>true}
|
49
|
+
when :on_exit
|
50
|
+
@triggers << {:from=>[state_name], :to=>["*"], :trigger=>Array(val), :type=>:on_exit}
|
51
|
+
when :on_exit_deferred
|
52
|
+
@triggers << {:from=>[state_name], :to=>["*"], :trigger=>Array(val), :type=>:on_exit, :deferred=>true}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
@states[state_name] = {:guards=>guards, :message=>message}
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
def on(pairs)
|
61
|
+
trigger = pairs.delete :trigger
|
62
|
+
deferred = pairs.delete :deferred
|
63
|
+
pairs.each do |from, to|
|
64
|
+
@triggers << {:from=>Array(from).map(&:to_s), :to=>Array(to).map(&:to_s),
|
65
|
+
:trigger=>Array(trigger), :type=>:trigger, :deferred=>!!deferred}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
def known?(to)
|
72
|
+
@states.keys.include? to
|
73
|
+
end
|
74
|
+
|
75
|
+
def allow?(record, to)
|
76
|
+
return false unless known?(to)
|
77
|
+
states[to][:guards].all? do |g|
|
78
|
+
case g
|
79
|
+
when Proc
|
80
|
+
g.call record
|
81
|
+
when Symbol, String
|
82
|
+
record.send g
|
83
|
+
else
|
84
|
+
raise ArgumentError, "Expecing Symbol or Proc for :guard, got #{g.class} : #{g}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def message(to)
|
90
|
+
states[to][:message]
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
def trigger(record, from, to, deferred=false)
|
95
|
+
# Rails.logger.debug "Checking triggers for transition #{from} to #{to} (deferred:#{deferred.inspect})"
|
96
|
+
@triggers.select do |trigger|
|
97
|
+
deferred ? trigger[:deferred] : !trigger[:deferred]
|
98
|
+
end.select do |trigger|
|
99
|
+
(trigger[:from].include?("*") || trigger[:from].include?(from)) &&
|
100
|
+
(trigger[:to].include?("*") || trigger[:to].include?(to))
|
101
|
+
# end.each do |trigger|
|
102
|
+
# Rails.logger.debug " Matched trigger: #{trigger[:from].inspect} -- #{trigger[:to].inspect}"
|
103
|
+
end.each do |trigger|
|
104
|
+
call_triggers record, trigger
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def call_triggers(record, trigger)
|
112
|
+
trigger[:trigger].each do |m|
|
113
|
+
case m
|
114
|
+
when Proc
|
115
|
+
m.call record
|
116
|
+
when Symbol, String
|
117
|
+
record.send m
|
118
|
+
else
|
119
|
+
raise ArgumentError, "Expecing Symbol or Proc for #{trigger[:type].inspect}, got #{m.class} : #{m}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module CanHasState
|
2
|
+
module Machine
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
def state_machine(column, &block)
|
8
|
+
d = Definition.new(column, &block)
|
9
|
+
|
10
|
+
define_method "allow_#{column}?" do |to|
|
11
|
+
state_machine_allow?(column.to_sym, to.to_s)
|
12
|
+
end
|
13
|
+
|
14
|
+
self.state_machines += [[column.to_sym, d]]
|
15
|
+
end
|
16
|
+
|
17
|
+
def extend_state_machine(column, &block)
|
18
|
+
sm = state_machines.detect{|(col, stm)| col == column}
|
19
|
+
raise("Unknown state machine #{column}") unless sm
|
20
|
+
sm[1].extend_machine &block
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
included do
|
26
|
+
unless method_defined? :state_machines
|
27
|
+
class_attribute :state_machines, :instance_writer=>false
|
28
|
+
self.state_machines = []
|
29
|
+
end
|
30
|
+
before_validation :can_has_initial_states
|
31
|
+
before_validation :can_has_state_triggers
|
32
|
+
validate :can_has_valid_state_machines
|
33
|
+
after_save :can_has_deferred_state_triggers
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def can_has_initial_states
|
40
|
+
state_machines.each do |(column, sm)|
|
41
|
+
if send(column).blank?
|
42
|
+
send("#{column}=", sm.initial_state)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def can_has_state_triggers
|
48
|
+
# skip triggers if any state machine isn't valid
|
49
|
+
return if can_has_state_errors.any?
|
50
|
+
|
51
|
+
@triggers_called ||= {}
|
52
|
+
state_machines.each do |(column, sm)|
|
53
|
+
from, to = send("#{column}_was"), send(column)
|
54
|
+
next if from == to
|
55
|
+
|
56
|
+
# skip triggers if they've already been called for this from/to transition
|
57
|
+
next if @triggers_called[column] == [from, to]
|
58
|
+
|
59
|
+
sm.trigger(self, from, to)
|
60
|
+
|
61
|
+
# record that triggers were called
|
62
|
+
@triggers_called[column] = [from, to]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def can_has_deferred_state_triggers
|
67
|
+
state_machines.each do |(column, sm)|
|
68
|
+
# clear record of called triggers
|
69
|
+
@triggers_called[column] = nil
|
70
|
+
|
71
|
+
from, to = send("#{column}_was"), send(column)
|
72
|
+
next if from == to
|
73
|
+
sm.trigger(self, from, to, :deferred)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def can_has_state_errors
|
78
|
+
err = []
|
79
|
+
state_machines.each do |(column, sm)|
|
80
|
+
from, to = send("#{column}_was"), send(column)
|
81
|
+
next if from == to
|
82
|
+
if !sm.known?(to)
|
83
|
+
err << [column, "is not a known state"]
|
84
|
+
elsif !sm.allow?(self, to) #state_machine_allow?(column, to)
|
85
|
+
err << [column, sm.message(to) % {:from=>from, :to=>to}]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
err
|
89
|
+
end
|
90
|
+
|
91
|
+
def can_has_valid_state_machines
|
92
|
+
can_has_state_errors.each do |(column, msg)|
|
93
|
+
errors.add column, msg
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def state_machine_allow?(column, to)
|
98
|
+
sm = state_machines.detect{|(col, stm)| col == column}
|
99
|
+
raise("Unknown state machine #{column}") unless sm
|
100
|
+
sm[1].allow?(self, to)
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|