state_machines-activemodel 0.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.
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