state_machines-activemodel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.idea/.rakeTasks +7 -0
  4. data/.idea/cssxfire.xml +9 -0
  5. data/.idea/encodings.xml +5 -0
  6. data/.idea/misc.xml +5 -0
  7. data/.idea/modules.xml +9 -0
  8. data/.idea/scopes/scope_settings.xml +5 -0
  9. data/.idea/state_machine2_activemodel.iml +32 -0
  10. data/.idea/vcs.xml +7 -0
  11. data/.idea/workspace.xml +50 -0
  12. data/.rspec +3 -0
  13. data/.travis.yml +19 -0
  14. data/Appraisals +34 -0
  15. data/Gemfile +4 -0
  16. data/LICENSE.txt +23 -0
  17. data/README.md +86 -0
  18. data/Rakefile +10 -0
  19. data/gemfiles/active_model_3.2.gemfile +7 -0
  20. data/gemfiles/active_model_3.2.gemfile.lock +50 -0
  21. data/gemfiles/active_model_4.0.gemfile +7 -0
  22. data/gemfiles/active_model_4.0.gemfile.lock +56 -0
  23. data/gemfiles/active_model_4.0_obs.gemfile +8 -0
  24. data/gemfiles/active_model_4.0_obs.gemfile.lock +59 -0
  25. data/gemfiles/active_model_4.1.gemfile +7 -0
  26. data/gemfiles/active_model_4.1.gemfile.lock +57 -0
  27. data/gemfiles/active_model_4.1_obs.gemfile +8 -0
  28. data/gemfiles/active_model_4.1_obs.gemfile.lock +60 -0
  29. data/gemfiles/active_model_edge.gemfile +7 -0
  30. data/gemfiles/active_model_edge.gemfile.lock +62 -0
  31. data/gemfiles/active_model_edge_obs.gemfile +8 -0
  32. data/gemfiles/active_model_edge_obs.gemfile.lock +65 -0
  33. data/lib/state_machines-activemodel.rb +1 -0
  34. data/lib/state_machines/integrations/active_model.rb +593 -0
  35. data/lib/state_machines/integrations/active_model/locale.rb +11 -0
  36. data/lib/state_machines/integrations/active_model/observer.rb +33 -0
  37. data/lib/state_machines/integrations/active_model/observer_update.rb +42 -0
  38. data/lib/state_machines/integrations/version.rb +7 -0
  39. data/spec/active_model_spec.rb +639 -0
  40. data/spec/integration_spec.rb +27 -0
  41. data/spec/observer_spec.rb +519 -0
  42. data/spec/spec_helper.rb +8 -0
  43. data/spec/support/en.yml +5 -0
  44. data/spec/support/helpers.rb +64 -0
  45. data/spec/support/migration_helpers.rb +43 -0
  46. data/state_machines-activemodel.gemspec +27 -0
  47. metadata +182 -0
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", "~> 4.0.0"
6
+ gem "rails-observers"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ state_machines-activemodel (0.0.1)
5
+ activemodel (>= 3.2)
6
+ state_machines
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (4.0.4)
12
+ activesupport (= 4.0.4)
13
+ builder (~> 3.1.0)
14
+ activesupport (4.0.4)
15
+ i18n (~> 0.6, >= 0.6.9)
16
+ minitest (~> 4.2)
17
+ multi_json (~> 1.3)
18
+ thread_safe (~> 0.1)
19
+ tzinfo (~> 0.3.37)
20
+ appraisal (1.0.0)
21
+ bundler
22
+ rake
23
+ thor (>= 0.14.0)
24
+ builder (3.1.4)
25
+ diff-lcs (1.2.5)
26
+ i18n (0.6.9)
27
+ minitest (4.7.5)
28
+ multi_json (1.9.3)
29
+ rails-observers (0.1.2)
30
+ activemodel (~> 4.0)
31
+ rake (10.3.1)
32
+ rspec (3.0.0.beta2)
33
+ rspec-core (= 3.0.0.beta2)
34
+ rspec-expectations (= 3.0.0.beta2)
35
+ rspec-mocks (= 3.0.0.beta2)
36
+ rspec-core (3.0.0.beta2)
37
+ rspec-support (= 3.0.0.beta2)
38
+ rspec-expectations (3.0.0.beta2)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (= 3.0.0.beta2)
41
+ rspec-mocks (3.0.0.beta2)
42
+ rspec-support (= 3.0.0.beta2)
43
+ rspec-support (3.0.0.beta2)
44
+ state_machines (0.0.1)
45
+ thor (0.19.1)
46
+ thread_safe (0.3.3)
47
+ tzinfo (0.3.39)
48
+
49
+ PLATFORMS
50
+ ruby
51
+
52
+ DEPENDENCIES
53
+ activemodel (~> 4.0.0)
54
+ appraisal (>= 1)
55
+ bundler (>= 1.6)
56
+ rails-observers
57
+ rake (>= 10)
58
+ rspec (= 3.0.0.beta2)
59
+ state_machines-activemodel!
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", "~> 4.1.0"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,57 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ state_machines-activemodel (0.0.1)
5
+ activemodel (>= 3.2)
6
+ state_machines
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (4.1.0)
12
+ activesupport (= 4.1.0)
13
+ builder (~> 3.1)
14
+ activesupport (4.1.0)
15
+ i18n (~> 0.6, >= 0.6.9)
16
+ json (~> 1.7, >= 1.7.7)
17
+ minitest (~> 5.1)
18
+ thread_safe (~> 0.1)
19
+ tzinfo (~> 1.1)
20
+ appraisal (1.0.0)
21
+ bundler
22
+ rake
23
+ thor (>= 0.14.0)
24
+ builder (3.2.2)
25
+ diff-lcs (1.2.5)
26
+ i18n (0.6.9)
27
+ json (1.8.1)
28
+ minitest (5.3.3)
29
+ rake (10.3.1)
30
+ rspec (3.0.0.beta2)
31
+ rspec-core (= 3.0.0.beta2)
32
+ rspec-expectations (= 3.0.0.beta2)
33
+ rspec-mocks (= 3.0.0.beta2)
34
+ rspec-core (3.0.0.beta2)
35
+ rspec-support (= 3.0.0.beta2)
36
+ rspec-expectations (3.0.0.beta2)
37
+ diff-lcs (>= 1.2.0, < 2.0)
38
+ rspec-support (= 3.0.0.beta2)
39
+ rspec-mocks (3.0.0.beta2)
40
+ rspec-support (= 3.0.0.beta2)
41
+ rspec-support (3.0.0.beta2)
42
+ state_machines (0.0.1)
43
+ thor (0.19.1)
44
+ thread_safe (0.3.3)
45
+ tzinfo (1.1.0)
46
+ thread_safe (~> 0.1)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ activemodel (~> 4.1.0)
53
+ appraisal (>= 1)
54
+ bundler (>= 1.6)
55
+ rake (>= 10)
56
+ rspec (= 3.0.0.beta2)
57
+ state_machines-activemodel!
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", "~> 4.1.0"
6
+ gem "rails-observers"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,60 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ state_machines-activemodel (0.0.1)
5
+ activemodel (>= 3.2)
6
+ state_machines
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (4.1.0)
12
+ activesupport (= 4.1.0)
13
+ builder (~> 3.1)
14
+ activesupport (4.1.0)
15
+ i18n (~> 0.6, >= 0.6.9)
16
+ json (~> 1.7, >= 1.7.7)
17
+ minitest (~> 5.1)
18
+ thread_safe (~> 0.1)
19
+ tzinfo (~> 1.1)
20
+ appraisal (1.0.0)
21
+ bundler
22
+ rake
23
+ thor (>= 0.14.0)
24
+ builder (3.2.2)
25
+ diff-lcs (1.2.5)
26
+ i18n (0.6.9)
27
+ json (1.8.1)
28
+ minitest (5.3.3)
29
+ rails-observers (0.1.2)
30
+ activemodel (~> 4.0)
31
+ rake (10.3.1)
32
+ rspec (3.0.0.beta2)
33
+ rspec-core (= 3.0.0.beta2)
34
+ rspec-expectations (= 3.0.0.beta2)
35
+ rspec-mocks (= 3.0.0.beta2)
36
+ rspec-core (3.0.0.beta2)
37
+ rspec-support (= 3.0.0.beta2)
38
+ rspec-expectations (3.0.0.beta2)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (= 3.0.0.beta2)
41
+ rspec-mocks (3.0.0.beta2)
42
+ rspec-support (= 3.0.0.beta2)
43
+ rspec-support (3.0.0.beta2)
44
+ state_machines (0.0.1)
45
+ thor (0.19.1)
46
+ thread_safe (0.3.3)
47
+ tzinfo (1.1.0)
48
+ thread_safe (~> 0.1)
49
+
50
+ PLATFORMS
51
+ ruby
52
+
53
+ DEPENDENCIES
54
+ activemodel (~> 4.1.0)
55
+ appraisal (>= 1)
56
+ bundler (>= 1.6)
57
+ rails-observers
58
+ rake (>= 10)
59
+ rspec (= 3.0.0.beta2)
60
+ state_machines-activemodel!
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", :github => "rails/rails"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,62 @@
1
+ GIT
2
+ remote: git://github.com/rails/rails.git
3
+ revision: 89893a000188f977163dddc04ff37846169a35dc
4
+ specs:
5
+ activemodel (4.2.0.alpha)
6
+ activesupport (= 4.2.0.alpha)
7
+ builder (~> 3.1)
8
+ activesupport (4.2.0.alpha)
9
+ i18n (~> 0.6, >= 0.6.9)
10
+ json (~> 1.7, >= 1.7.7)
11
+ minitest (~> 5.1)
12
+ thread_safe (~> 0.1)
13
+ tzinfo (~> 1.1)
14
+
15
+ PATH
16
+ remote: ../
17
+ specs:
18
+ state_machines-activemodel (0.0.1)
19
+ activemodel (>= 3.2)
20
+ state_machines
21
+
22
+ GEM
23
+ remote: https://rubygems.org/
24
+ specs:
25
+ appraisal (1.0.0)
26
+ bundler
27
+ rake
28
+ thor (>= 0.14.0)
29
+ builder (3.2.2)
30
+ diff-lcs (1.2.5)
31
+ i18n (0.6.9)
32
+ json (1.8.1)
33
+ minitest (5.3.3)
34
+ rake (10.3.1)
35
+ rspec (3.0.0.beta2)
36
+ rspec-core (= 3.0.0.beta2)
37
+ rspec-expectations (= 3.0.0.beta2)
38
+ rspec-mocks (= 3.0.0.beta2)
39
+ rspec-core (3.0.0.beta2)
40
+ rspec-support (= 3.0.0.beta2)
41
+ rspec-expectations (3.0.0.beta2)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (= 3.0.0.beta2)
44
+ rspec-mocks (3.0.0.beta2)
45
+ rspec-support (= 3.0.0.beta2)
46
+ rspec-support (3.0.0.beta2)
47
+ state_machines (0.0.1)
48
+ thor (0.19.1)
49
+ thread_safe (0.3.3)
50
+ tzinfo (1.1.0)
51
+ thread_safe (~> 0.1)
52
+
53
+ PLATFORMS
54
+ ruby
55
+
56
+ DEPENDENCIES
57
+ activemodel!
58
+ appraisal (>= 1)
59
+ bundler (>= 1.6)
60
+ rake (>= 10)
61
+ rspec (= 3.0.0.beta2)
62
+ state_machines-activemodel!
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", :github => "rails/rails"
6
+ gem "rails-observers"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,65 @@
1
+ GIT
2
+ remote: git://github.com/rails/rails.git
3
+ revision: 89893a000188f977163dddc04ff37846169a35dc
4
+ specs:
5
+ activemodel (4.2.0.alpha)
6
+ activesupport (= 4.2.0.alpha)
7
+ builder (~> 3.1)
8
+ activesupport (4.2.0.alpha)
9
+ i18n (~> 0.6, >= 0.6.9)
10
+ json (~> 1.7, >= 1.7.7)
11
+ minitest (~> 5.1)
12
+ thread_safe (~> 0.1)
13
+ tzinfo (~> 1.1)
14
+
15
+ PATH
16
+ remote: ../
17
+ specs:
18
+ state_machines-activemodel (0.0.1)
19
+ activemodel (>= 3.2)
20
+ state_machines
21
+
22
+ GEM
23
+ remote: https://rubygems.org/
24
+ specs:
25
+ appraisal (1.0.0)
26
+ bundler
27
+ rake
28
+ thor (>= 0.14.0)
29
+ builder (3.2.2)
30
+ diff-lcs (1.2.5)
31
+ i18n (0.6.9)
32
+ json (1.8.1)
33
+ minitest (5.3.3)
34
+ rails-observers (0.1.2)
35
+ activemodel (~> 4.0)
36
+ rake (10.3.1)
37
+ rspec (3.0.0.beta2)
38
+ rspec-core (= 3.0.0.beta2)
39
+ rspec-expectations (= 3.0.0.beta2)
40
+ rspec-mocks (= 3.0.0.beta2)
41
+ rspec-core (3.0.0.beta2)
42
+ rspec-support (= 3.0.0.beta2)
43
+ rspec-expectations (3.0.0.beta2)
44
+ diff-lcs (>= 1.2.0, < 2.0)
45
+ rspec-support (= 3.0.0.beta2)
46
+ rspec-mocks (3.0.0.beta2)
47
+ rspec-support (= 3.0.0.beta2)
48
+ rspec-support (3.0.0.beta2)
49
+ state_machines (0.0.1)
50
+ thor (0.19.1)
51
+ thread_safe (0.3.3)
52
+ tzinfo (1.1.0)
53
+ thread_safe (~> 0.1)
54
+
55
+ PLATFORMS
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ activemodel!
60
+ appraisal (>= 1)
61
+ bundler (>= 1.6)
62
+ rails-observers
63
+ rake (>= 10)
64
+ rspec (= 3.0.0.beta2)
65
+ state_machines-activemodel!
@@ -0,0 +1 @@
1
+ require 'state_machines/integrations/active_model'
@@ -0,0 +1,593 @@
1
+ require 'active_model'
2
+ require 'active_support/all'
3
+ require 'state_machines'
4
+ require 'state_machines/integrations/version'
5
+
6
+ module StateMachines
7
+ module Integrations #:nodoc:
8
+ # Adds support for integrating state machines with ActiveModel classes.
9
+ #
10
+ # == Examples
11
+ #
12
+ # If using ActiveModel directly within your class, then any one of the
13
+ # following features need to be included in order for the integration to be
14
+ # detected:
15
+ # * ActiveModel::Observing
16
+ # * ActiveModel::Validations
17
+ #
18
+ # Below is an example of a simple state machine defined within an
19
+ # ActiveModel class:
20
+ #
21
+ # class Vehicle
22
+ # include ActiveModel::Observing
23
+ # include ActiveModel::Validations
24
+ #
25
+ # attr_accessor :state
26
+ # define_attribute_methods [:state]
27
+ #
28
+ # state_machine :initial => :parked do
29
+ # event :ignite do
30
+ # transition :parked => :idling
31
+ # end
32
+ # end
33
+ # end
34
+ #
35
+ # The examples in the sections below will use the above class as a
36
+ # reference.
37
+ #
38
+ # == Actions
39
+ #
40
+ # By default, no action will be invoked when a state is transitioned. This
41
+ # means that if you want to save changes when transitioning, you must
42
+ # define the action yourself like so:
43
+ #
44
+ # class Vehicle
45
+ # include ActiveModel::Validations
46
+ # attr_accessor :state
47
+ #
48
+ # state_machine :action => :save do
49
+ # ...
50
+ # end
51
+ #
52
+ # def save
53
+ # # Save changes
54
+ # end
55
+ # end
56
+ #
57
+ # == Validations
58
+ #
59
+ # As mentioned in StateMachine::Machine#state, you can define behaviors,
60
+ # like validations, that only execute for certain states. One *important*
61
+ # caveat here is that, due to a constraint in ActiveModel's validation
62
+ # framework, custom validators will not work as expected when defined to run
63
+ # in multiple states. For example:
64
+ #
65
+ # class Vehicle
66
+ # include ActiveModel::Validations
67
+ #
68
+ # state_machine do
69
+ # ...
70
+ # state :first_gear, :second_gear do
71
+ # validate :speed_is_legal
72
+ # end
73
+ # end
74
+ # end
75
+ #
76
+ # In this case, the <tt>:speed_is_legal</tt> validation will only get run
77
+ # for the <tt>:second_gear</tt> state. To avoid this, you can define your
78
+ # custom validation like so:
79
+ #
80
+ # class Vehicle
81
+ # include ActiveModel::Validations
82
+ #
83
+ # state_machine do
84
+ # ...
85
+ # state :first_gear, :second_gear do
86
+ # validate {|vehicle| vehicle.speed_is_legal}
87
+ # end
88
+ # end
89
+ # end
90
+ #
91
+ # == Validation errors
92
+ #
93
+ # In order to hook in validation support for your model, the
94
+ # ActiveModel::Validations feature must be included. If this is included
95
+ # and an event fails to successfully fire because there are no matching
96
+ # transitions for the object, a validation error is added to the object's
97
+ # state attribute to help in determining why it failed.
98
+ #
99
+ # For example,
100
+ #
101
+ # vehicle = Vehicle.new
102
+ # vehicle.ignite # => false
103
+ # vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
104
+ #
105
+ # In addition, if you're using the <tt>ignite!</tt> version of the event,
106
+ # then the failure reason (such as the current validation errors) will be
107
+ # included in the exception that gets raised when the event fails. For
108
+ # example, assuming there's a validation on a field called +name+ on the class:
109
+ #
110
+ # vehicle = Vehicle.new
111
+ # vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
112
+ #
113
+ # === Security implications
114
+ #
115
+ # Beware that public event attributes mean that events can be fired
116
+ # whenever mass-assignment is being used. If you want to prevent malicious
117
+ # users from tampering with events through URLs / forms, the attribute
118
+ # should be protected like so:
119
+ #
120
+ # class Vehicle
121
+ # include ActiveModel::MassAssignmentSecurity
122
+ # attr_accessor :state
123
+ #
124
+ # attr_protected :state_event
125
+ # # attr_accessible ... # Alternative technique
126
+ #
127
+ # state_machine do
128
+ # ...
129
+ # end
130
+ # end
131
+ #
132
+ # If you want to only have *some* events be able to fire via mass-assignment,
133
+ # you can build two state machines (one public and one protected) like so:
134
+ #
135
+ # class Vehicle
136
+ # include ActiveModel::MassAssignmentSecurity
137
+ # attr_accessor :state
138
+ #
139
+ # attr_protected :state_event # Prevent access to events in the first machine
140
+ #
141
+ # state_machine do
142
+ # # Define private events here
143
+ # end
144
+ #
145
+ # # Public machine targets the same state as the private machine
146
+ # state_machine :public_state, :attribute => :state do
147
+ # # Define public events here
148
+ # end
149
+ # end
150
+ #
151
+ # == Callbacks
152
+ #
153
+ # All before/after transition callbacks defined for ActiveModel models
154
+ # behave in the same way that other ActiveSupport callbacks behave. The
155
+ # object involved in the transition is passed in as an argument.
156
+ #
157
+ # For example,
158
+ #
159
+ # class Vehicle
160
+ # include ActiveModel::Validations
161
+ # attr_accessor :state
162
+ #
163
+ # state_machine :initial => :parked do
164
+ # before_transition any => :idling do |vehicle|
165
+ # vehicle.put_on_seatbelt
166
+ # end
167
+ #
168
+ # before_transition do |vehicle, transition|
169
+ # # log message
170
+ # end
171
+ #
172
+ # event :ignite do
173
+ # transition :parked => :idling
174
+ # end
175
+ # end
176
+ #
177
+ # def put_on_seatbelt
178
+ # ...
179
+ # end
180
+ # end
181
+ #
182
+ # Note, also, that the transition can be accessed by simply defining
183
+ # additional arguments in the callback block.
184
+ #
185
+ # == Observers
186
+ #
187
+ # In order to hook in observer support for your application, the
188
+ # ActiveModel::Observing feature must be included. Because of the way
189
+ # ActiveModel observers are designed, there is less flexibility around the
190
+ # specific transitions that can be hooked in. However, a large number of
191
+ # hooks *are* supported. For example, if a transition for a object's
192
+ # +state+ attribute changes the state from +parked+ to +idling+ via the
193
+ # +ignite+ event, the following observer methods are supported:
194
+ # * before/after/after_failure_to-_ignite_from_parked_to_idling
195
+ # * before/after/after_failure_to-_ignite_from_parked
196
+ # * before/after/after_failure_to-_ignite_to_idling
197
+ # * before/after/after_failure_to-_ignite
198
+ # * before/after/after_failure_to-_transition_state_from_parked_to_idling
199
+ # * before/after/after_failure_to-_transition_state_from_parked
200
+ # * before/after/after_failure_to-_transition_state_to_idling
201
+ # * before/after/after_failure_to-_transition_state
202
+ # * before/after/after_failure_to-_transition
203
+ #
204
+ # The following class shows an example of some of these hooks:
205
+ #
206
+ # class VehicleObserver < ActiveModel::Observer
207
+ # # Callback for :ignite event *before* the transition is performed
208
+ # def before_ignite(vehicle, transition)
209
+ # # log message
210
+ # end
211
+ #
212
+ # # Callback for :ignite event *after* the transition has been performed
213
+ # def after_ignite(vehicle, transition)
214
+ # # put on seatbelt
215
+ # end
216
+ #
217
+ # # Generic transition callback *before* the transition is performed
218
+ # def after_transition(vehicle, transition)
219
+ # Audit.log(vehicle, transition)
220
+ # end
221
+ #
222
+ # def after_failure_to_transition(vehicle, transition)
223
+ # Audit.error(vehicle, transition)
224
+ # end
225
+ # end
226
+ #
227
+ # More flexible transition callbacks can be defined directly within the
228
+ # model as described in StateMachine::Machine#before_transition
229
+ # and StateMachine::Machine#after_transition.
230
+ #
231
+ # To define a single observer for multiple state machines:
232
+ #
233
+ # class StateMachineObserver < ActiveModel::Observer
234
+ # observe Vehicle, Switch, Project
235
+ #
236
+ # def after_transition(object, transition)
237
+ # Audit.log(object, transition)
238
+ # end
239
+ # end
240
+ #
241
+ # == Internationalization
242
+ #
243
+ # Any error message that is generated from performing invalid transitions
244
+ # can be localized. The following default translations are used:
245
+ #
246
+ # en:
247
+ # activemodel:
248
+ # errors:
249
+ # messages:
250
+ # invalid: "is invalid"
251
+ # # %{value} = attribute value, %{state} = Human state name
252
+ # invalid_event: "cannot transition when %{state}"
253
+ # # %{value} = attribute value, %{event} = Human event name, %{state} = Human current state name
254
+ # invalid_transition: "cannot transition via %{event}"
255
+ #
256
+ # You can override these for a specific model like so:
257
+ #
258
+ # en:
259
+ # activemodel:
260
+ # errors:
261
+ # models:
262
+ # user:
263
+ # invalid: "is not valid"
264
+ #
265
+ # In addition to the above, you can also provide translations for the
266
+ # various states / events in each state machine. Using the Vehicle example,
267
+ # state translations will be looked for using the following keys, where
268
+ # +model_name+ = "vehicle", +machine_name+ = "state" and +state_name+ = "parked":
269
+ # * <tt>activemodel.state_machines.#{model_name}.#{machine_name}.states.#{state_name}</tt>
270
+ # * <tt>activemodel.state_machines.#{model_name}.states.#{state_name}</tt>
271
+ # * <tt>activemodel.state_machines.#{machine_name}.states.#{state_name}</tt>
272
+ # * <tt>activemodel.state_machines.states.#{state_name}</tt>
273
+ #
274
+ # Event translations will be looked for using the following keys, where
275
+ # +model_name+ = "vehicle", +machine_name+ = "state" and +event_name+ = "ignite":
276
+ # * <tt>activemodel.state_machines.#{model_name}.#{machine_name}.events.#{event_name}</tt>
277
+ # * <tt>activemodel.state_machines.#{model_name}.events.#{event_name}</tt>
278
+ # * <tt>activemodel.state_machines.#{machine_name}.events.#{event_name}</tt>
279
+ # * <tt>activemodel.state_machines.events.#{event_name}</tt>
280
+ #
281
+ # An example translation configuration might look like so:
282
+ #
283
+ # es:
284
+ # activemodel:
285
+ # state_machines:
286
+ # states:
287
+ # parked: 'estacionado'
288
+ # events:
289
+ # park: 'estacionarse'
290
+ #
291
+ # == Dirty Attribute Tracking
292
+ #
293
+ # When using the ActiveModel::Dirty extension, your model will keep track of
294
+ # any changes that are made to attributes. Depending on your ORM, an object
295
+ # will only be saved when there are attributes that have changed on the
296
+ # object. When integrating with state_machine, typically the +state+ field
297
+ # will be marked as dirty after a transition occurs. In some situations,
298
+ # however, this isn't the case.
299
+ #
300
+ # If you define loopback transitions in your state machine, the value for
301
+ # the machine's attribute (e.g. state) will not change. Unless you explicitly
302
+ # indicate so, this means that your object won't persist anything on a
303
+ # loopback. For example:
304
+ #
305
+ # class Vehicle
306
+ # include ActiveModel::Validations
307
+ # include ActiveModel::Dirty
308
+ # attr_accessor :state
309
+ #
310
+ # state_machine :initial => :parked do
311
+ # event :park do
312
+ # transition :parked => :parked, ...
313
+ # end
314
+ # end
315
+ # end
316
+ #
317
+ # If, instead, you'd like your object to always persist regardless of
318
+ # whether the value actually changed, you can do so by using the
319
+ # <tt>#{attribute}_will_change!</tt> helpers or defining a +before_transition+
320
+ # callback that actually changes an attribute on the model. For example:
321
+ #
322
+ # class Vehicle
323
+ # ...
324
+ # state_machine :initial => :parked do
325
+ # before_transition all => same do |vehicle|
326
+ # vehicle.state_will_change!
327
+ #
328
+ # # Alternative solution, updating timestamp
329
+ # # vehicle.updated_at = Time.curent
330
+ # end
331
+ # end
332
+ # end
333
+ #
334
+ # == Creating new integrations
335
+ #
336
+ # If you want to integrate state_machine with an ORM that implements parts
337
+ # or all of the ActiveModel API, only the machine defaults need to be
338
+ # specified. Otherwise, the implementation is similar to any other
339
+ # integration.
340
+ #
341
+ # For example,
342
+ #
343
+ # module StateMachine::Integrations::MyORM
344
+ # include StateMachine::Integrations::ActiveModel
345
+ #
346
+ # @defaults = {:action => :persist}
347
+ #
348
+ # def self.matches?(klass)
349
+ # defined?(::MyORM::Base) && klass <= ::MyORM::Base
350
+ # end
351
+ #
352
+ # protected
353
+ # def runs_validations_on_action?
354
+ # action == :persist
355
+ # end
356
+ # end
357
+ #
358
+ # If you wish to implement other features, such as attribute initialization
359
+ # with protected attributes, named scopes, or database transactions, you
360
+ # must add these independent of the ActiveModel integration. See the
361
+ # ActiveRecord implementation for examples of these customizations.
362
+ module ActiveModel
363
+
364
+ include StateMachines::Integrations::Base
365
+ extend ClassMethods
366
+
367
+
368
+ @defaults = {}
369
+
370
+ # Classes that include ActiveModel::Observing or ActiveModel::Validations
371
+ # will automatically use the ActiveModel integration.
372
+ def self.matching_ancestors
373
+ %w(ActiveModel ActiveModel::Observing ActiveModel::Validations)
374
+ end
375
+
376
+ # Adds a validation error to the given object
377
+ def invalidate(object, attribute, message, values = [])
378
+ if supports_validations?
379
+ attribute = self.attribute(attribute)
380
+ options = values.inject({}) do |h, (key, value)|
381
+ h[key] = value
382
+ h
383
+ end
384
+
385
+ default_options = default_error_message_options(object, attribute, message)
386
+ object.errors.add(attribute, message, options.merge(default_options))
387
+ end
388
+ end
389
+
390
+ # Describes the current validation errors on the given object. If none
391
+ # are specific, then the default error is interpeted as a "halt".
392
+ def errors_for(object)
393
+ object.errors.empty? ? 'Transition halted' : object.errors.full_messages * ', '
394
+ end
395
+
396
+ # Resets any errors previously added when invalidating the given object
397
+ def reset(object)
398
+ object.errors.clear if supports_validations?
399
+ end
400
+
401
+ # Runs state events around the object's validation process
402
+ def around_validation(object)
403
+ object.class.state_machines.transitions(object, action, :after => false).perform { yield }
404
+ end
405
+
406
+ protected
407
+ # Whether observers are supported in the integration. Only true if
408
+ # ActiveModel::Observer is available.
409
+ def supports_observers?
410
+ defined?(::ActiveModel::Observing) && owner_class <= ::ActiveModel::Observing
411
+ end
412
+
413
+ # Whether validations are supported in the integration. Only true if
414
+ # the ActiveModel feature is enabled on the owner class.
415
+ def supports_validations?
416
+ defined?(::ActiveModel::Validations) && owner_class <= ::ActiveModel::Validations
417
+ end
418
+
419
+ # Do validations run when the action configured this machine is
420
+ # invoked? This is used to determine whether to fire off attribute-based
421
+ # event transitions when the action is run.
422
+ def runs_validations_on_action?
423
+ false
424
+ end
425
+
426
+ # Gets the terminator to use for callbacks
427
+ def callback_terminator
428
+ @terminator ||= lambda { |result| result == false }
429
+ end
430
+
431
+ # Determines the base scope to use when looking up translations
432
+ def i18n_scope(klass)
433
+ klass.i18n_scope
434
+ end
435
+
436
+ # The default options to use when generating messages for validation
437
+ # errors
438
+ def default_error_message_options(object, attribute, message)
439
+ {:message => @messages[message]}
440
+ end
441
+
442
+ # Translates the given key / value combo. Translation keys are looked
443
+ # up in the following order:
444
+ # * <tt>#{i18n_scope}.state_machines.#{model_name}.#{machine_name}.#{plural_key}.#{value}</tt>
445
+ # * <tt>#{i18n_scope}.state_machines.#{model_name}.#{plural_key}.#{value}</tt>
446
+ # * <tt>#{i18n_scope}.state_machines.#{machine_name}.#{plural_key}.#{value}</tt>
447
+ # * <tt>#{i18n_scope}.state_machines.#{plural_key}.#{value}</tt>
448
+ #
449
+ # If no keys are found, then the humanized value will be the fallback.
450
+ def translate(klass, key, value)
451
+ ancestors = ancestors_for(klass)
452
+ group = key.to_s.pluralize
453
+ value = value ? value.to_s : 'nil'
454
+
455
+ # Generate all possible translation keys
456
+ translations = ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}" }
457
+ translations.concat(ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}" })
458
+ translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
459
+ I18n.translate(translations.shift, :default => translations, :scope => [i18n_scope(klass), :state_machines])
460
+ end
461
+
462
+ # Build a list of ancestors for the given class to use when
463
+ # determining which localization key to use for a particular string.
464
+ def ancestors_for(klass)
465
+ load_observer_extensions
466
+ klass.lookup_ancestors
467
+ end
468
+
469
+ # Initializes class-level extensions and defaults for this machine
470
+ def after_initialize
471
+ super()
472
+ load_locale
473
+ if supports_observers?
474
+ load_observer_extensions
475
+ add_default_callbacks
476
+ end
477
+ end
478
+
479
+ # Loads any locale files needed for translating validation errors
480
+ def load_locale
481
+ I18n.load_path.unshift(locale_path) unless I18n.load_path.include?(locale_path)
482
+ end
483
+
484
+ def locale_path
485
+ "#{File.dirname(__FILE__)}/active_model/locale.rb"
486
+ end
487
+
488
+ # Loads extensions to ActiveModel's Observers
489
+ def load_observer_extensions
490
+ require 'state_machines/integrations/active_model/observer'
491
+ require 'state_machines/integrations/active_model/observer_update'
492
+ end
493
+
494
+ # Adds a set of default callbacks that utilize the Observer extensions
495
+ def add_default_callbacks
496
+ if supports_observers?
497
+ callbacks[:before] << Callback.new(:before) { |object, transition| notify(:before, object, transition) }
498
+ callbacks[:after] << Callback.new(:after) { |object, transition| notify(:after, object, transition) }
499
+ callbacks[:failure] << Callback.new(:failure) { |object, transition| notify(:after_failure_to, object, transition) }
500
+ end
501
+ end
502
+
503
+ # Skips defining reader/writer methods since this is done automatically
504
+ def define_state_accessor
505
+ name = self.name
506
+
507
+ owner_class.validates_each(attribute) do |object, attr, value|
508
+ machine = object.class.state_machine(name)
509
+ machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
510
+ end if supports_validations?
511
+ end
512
+
513
+ # Adds hooks into validation for automatically firing events
514
+ def define_action_helpers
515
+ super
516
+ define_validation_hook if runs_validations_on_action?
517
+ end
518
+
519
+ # Hooks into validations by defining around callbacks for the
520
+ # :validation event
521
+ def define_validation_hook
522
+ owner_class.set_callback(:validation, :around, self, :prepend => true)
523
+ end
524
+
525
+ # Creates a new callback in the callback chain, always inserting it
526
+ # before the default Observer callbacks that were created after
527
+ # initialization.
528
+ def add_callback(type, options, &block)
529
+ options[:terminator] = callback_terminator
530
+
531
+ if supports_observers?
532
+ @callbacks[type == :around ? :before : type].insert(-2, callback = Callback.new(type, options, &block))
533
+ add_states(callback.known_states)
534
+ callback
535
+ else
536
+ super
537
+ end
538
+ end
539
+
540
+ # Configures new states with the built-in humanize scheme
541
+ def add_states(new_states)
542
+ super.each do |new_state|
543
+ new_state.human_name = lambda { |state, klass| translate(klass, :state, state.name) }
544
+ end
545
+ end
546
+
547
+ # Configures new event with the built-in humanize scheme
548
+ def add_events(new_events)
549
+ super.each do |new_event|
550
+ new_event.human_name = lambda { |event, klass| translate(klass, :event, event.name) }
551
+ end
552
+ end
553
+
554
+ # Notifies observers on the given object that a callback occurred
555
+ # involving the given transition. This will attempt to call the
556
+ # following methods on observers:
557
+ # * <tt>#{type}_#{qualified_event}_from_#{from}_to_#{to}</tt>
558
+ # * <tt>#{type}_#{qualified_event}_from_#{from}</tt>
559
+ # * <tt>#{type}_#{qualified_event}_to_#{to}</tt>
560
+ # * <tt>#{type}_#{qualified_event}</tt>
561
+ # * <tt>#{type}_transition_#{machine_name}_from_#{from}_to_#{to}</tt>
562
+ # * <tt>#{type}_transition_#{machine_name}_from_#{from}</tt>
563
+ # * <tt>#{type}_transition_#{machine_name}_to_#{to}</tt>
564
+ # * <tt>#{type}_transition_#{machine_name}</tt>
565
+ # * <tt>#{type}_transition</tt>
566
+ #
567
+ # This will always return true regardless of the results of the
568
+ # callbacks.
569
+ def notify(type, object, transition)
570
+ name = self.name
571
+ event = transition.qualified_event
572
+ from = transition.from_name || 'nil'
573
+ to = transition.to_name || 'nil'
574
+
575
+ # Machine-specific updates
576
+ ["#{type}_#{event}", "#{type}_transition_#{name}"].each do |event_segment|
577
+ ["_from_#{from}", nil].each do |from_segment|
578
+ ["_to_#{to}", nil].each do |to_segment|
579
+ object.class.changed if object.class.respond_to?(:changed)
580
+ object.class.notify_observers('update_with_transition', ObserverUpdate.new([event_segment, from_segment, to_segment].join, object, transition))
581
+ end
582
+ end
583
+ end
584
+
585
+ # Generic updates
586
+ object.class.changed if object.class.respond_to?(:changed)
587
+ object.class.notify_observers('update_with_transition', ObserverUpdate.new("#{type}_transition", object, transition))
588
+
589
+ true
590
+ end
591
+ end
592
+ end
593
+ end