state_machines-activerecord 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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.travis.yml +20 -0
  4. data/Appraisals +11 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE.txt +23 -0
  7. data/README.md +65 -0
  8. data/Rakefile +9 -0
  9. data/gemfiles/active_record_4.1.gemfile +13 -0
  10. data/gemfiles/active_record_4.2.gemfile +13 -0
  11. data/lib/state_machines-activerecord.rb +1 -0
  12. data/lib/state_machines/integrations/active_record.rb +588 -0
  13. data/lib/state_machines/integrations/active_record/locale.rb +12 -0
  14. data/lib/state_machines/integrations/active_record/version.rb +7 -0
  15. data/log/.gitkeep +0 -0
  16. data/state_machines-activerecord.gemspec +27 -0
  17. data/test/files/en.yml +5 -0
  18. data/test/files/models/post.rb +11 -0
  19. data/test/integration_test.rb +23 -0
  20. data/test/machine_by_default_test.rb +16 -0
  21. data/test/machine_errors_test.rb +19 -0
  22. data/test/machine_multiple_test.rb +17 -0
  23. data/test/machine_nested_action_test.rb +38 -0
  24. data/test/machine_unmigrated_test.rb +14 -0
  25. data/test/machine_with_aliased_attribute_test.rb +23 -0
  26. data/test/machine_with_callbacks_test.rb +172 -0
  27. data/test/machine_with_column_state_attribute_test.rb +44 -0
  28. data/test/machine_with_complex_pluralization_scopes_test.rb +16 -0
  29. data/test/machine_with_conflicting_predicate_test.rb +18 -0
  30. data/test/machine_with_conflicting_state_name_test.rb +29 -0
  31. data/test/machine_with_custom_attribute_test.rb +21 -0
  32. data/test/machine_with_default_scope_test.rb +18 -0
  33. data/test/machine_with_different_column_default_test.rb +27 -0
  34. data/test/machine_with_different_integer_column_default_test.rb +29 -0
  35. data/test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb +24 -0
  36. data/test/machine_with_dirty_attribute_and_state_events_test.rb +20 -0
  37. data/test/machine_with_dirty_attributes_and_custom_attribute_test.rb +32 -0
  38. data/test/machine_with_dirty_attributes_during_loopback_test.rb +22 -0
  39. data/test/machine_with_dirty_attributes_test.rb +35 -0
  40. data/test/machine_with_dynamic_initial_state_test.rb +99 -0
  41. data/test/machine_with_event_attributes_on_autosave_test.rb +48 -0
  42. data/test/machine_with_event_attributes_on_custom_action_test.rb +41 -0
  43. data/test/machine_with_event_attributes_on_save_bang_test.rb +82 -0
  44. data/test/machine_with_event_attributes_on_save_test.rb +184 -0
  45. data/test/machine_with_event_attributes_on_validation_test.rb +126 -0
  46. data/test/machine_with_events_test.rb +13 -0
  47. data/test/machine_with_failed_action_test.rb +40 -0
  48. data/test/machine_with_failed_after_callbacks_test.rb +35 -0
  49. data/test/machine_with_failed_before_callbacks_test.rb +36 -0
  50. data/test/machine_with_initialized_state_test.rb +35 -0
  51. data/test/machine_with_internationalization_test.rb +180 -0
  52. data/test/machine_with_loopback_test.rb +22 -0
  53. data/test/machine_with_non_column_state_attribute_defined_test.rb +35 -0
  54. data/test/machine_with_non_column_state_attribute_undefined_test.rb +33 -0
  55. data/test/machine_with_same_column_default_test.rb +26 -0
  56. data/test/machine_with_scopes_and_joins_test.rb +37 -0
  57. data/test/machine_with_scopes_and_owner_subclass_test.rb +27 -0
  58. data/test/machine_with_scopes_test.rb +70 -0
  59. data/test/machine_with_state_driven_validations_test.rb +30 -0
  60. data/test/machine_with_states_test.rb +13 -0
  61. data/test/machine_with_static_initial_state_test.rb +166 -0
  62. data/test/machine_with_transactions_test.rb +26 -0
  63. data/test/machine_with_validations_and_custom_attribute_test.rb +21 -0
  64. data/test/machine_with_validations_test.rb +47 -0
  65. data/test/machine_without_database_test.rb +20 -0
  66. data/test/machine_without_transactions_test.rb +26 -0
  67. data/test/model_test.rb +12 -0
  68. data/test/test_helper.rb +42 -0
  69. metadata +264 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8a08515c6e6c44049b7473b3140fd0e96f3eb3d8
4
+ data.tar.gz: 1722de2ecf4eb65aee81221d5725c49bcac361ec
5
+ SHA512:
6
+ metadata.gz: 43826d053ae3d06a3c2fa966245b70e6a56e71223567f7e4f1f622136016cbb24da5bae787c20f07d87f85645a8fcdad37bde872ae7778a334e38298fa87465e
7
+ data.tar.gz: a5cee9e813ce7adebed5027b0b68ba64be155f54db35df8943282556cab82d9fa89c467bdf0109879e8a3ea0bc8844532b9e8e86e73ed3377b7c58c2d5a277d5
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ tmp
16
+ *.bundle
17
+ *.so
18
+ *.o
19
+ *.a
20
+ log/active_record.log
21
+ .idea/
22
+ *.lock
data/.travis.yml ADDED
@@ -0,0 +1,20 @@
1
+ language: ruby
2
+ sudo: false
3
+ cache: bundler
4
+
5
+ rvm:
6
+ - 2.1
7
+ - 2.2
8
+ - jruby-19mode
9
+ - jruby
10
+ - rbx-2
11
+
12
+ gemfile:
13
+ - gemfiles/active_record_4.1.gemfile
14
+ - gemfiles/active_record_4.2.gemfile
15
+
16
+ matrix:
17
+ allow_failures:
18
+ - rvm: jruby
19
+ - rvm: jruby-19mode
20
+ - rvm: rbx-2
data/Appraisals ADDED
@@ -0,0 +1,11 @@
1
+ appraise "active_record_4.1" do
2
+ gem "sqlite3", platforms: [:mri, :rbx]
3
+ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
4
+ gem "activerecord", github: 'rails/rails', branch: '4-1-stable'
5
+ end
6
+
7
+ appraise "active_record_4.2" do
8
+ gem "sqlite3", platforms: [:mri, :rbx]
9
+ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
10
+ gem "activerecord", github: 'rails/rails', branch: '4-2-stable'
11
+ end
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ platforms :mri_20, :mri_21 do
5
+ gem 'pry-byebug'
6
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2006-2012 Aaron Pfeifer
2
+ Copyright (c) 2014-2015 Abdelkader Boudih
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ [![Build Status](https://travis-ci.org/state-machines/state_machines-activerecord.svg?branch=master)](https://travis-ci.org/state-machines/state_machines-activerecord)
2
+ [![Code Climate](https://codeclimate.com/github/state-machines/state_machines-activerecord.png)](https://codeclimate.com/github/state-machines/state_machines-activerecord)
3
+
4
+ # StateMachines Active Record Integration
5
+
6
+ The Active Record integration adds support for database transactions, automatically
7
+ saving the record, named scopes, validation errors.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'state_machines-activerecord'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install state_machines-activerecord
22
+
23
+ ## Usage
24
+
25
+ ```ruby
26
+ class Vehicle < ActiveRecord::Base
27
+ state_machine :initial => :parked do
28
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
29
+ after_transition any => :parked do |vehicle, transition|
30
+ vehicle.seatbelt = 'off'
31
+ end
32
+ around_transition :benchmark
33
+
34
+ event :ignite do
35
+ transition :parked => :idling
36
+ end
37
+
38
+ state :first_gear, :second_gear do
39
+ validates_presence_of :seatbelt_on
40
+ end
41
+ end
42
+
43
+ def put_on_seatbelt
44
+ ...
45
+ end
46
+
47
+ def benchmark
48
+ ...
49
+ yield
50
+ ...
51
+ end
52
+ end
53
+ ```
54
+
55
+ Dependencies
56
+
57
+ Active Record 4.1+
58
+
59
+ ## Contributing
60
+
61
+ 1. Fork it ( https://github.com/state-machines/state_machines-activerecord/fork )
62
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
63
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
64
+ 4. Push to the branch (`git push origin my-new-feature`)
65
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.pattern = 'test/*_test.rb'
6
+ end
7
+
8
+ desc 'Default: run all tests.'
9
+ task :default => :test
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", :platforms => [:mri, :rbx]
6
+ gem "activerecord-jdbcsqlite3-adapter", :platform => :jruby
7
+ gem "activerecord", :github => "rails/rails", :branch => "4-1-stable"
8
+
9
+ platforms :mri_20, :mri_21 do
10
+ gem "pry-byebug"
11
+ end
12
+
13
+ gemspec :path => "../"
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", :platforms => [:mri, :rbx]
6
+ gem "activerecord-jdbcsqlite3-adapter", :platform => :jruby
7
+ gem "activerecord", :github => "rails/rails", :branch => "4-2-stable"
8
+
9
+ platforms :mri_20, :mri_21 do
10
+ gem "pry-byebug"
11
+ end
12
+
13
+ gemspec :path => "../"
@@ -0,0 +1 @@
1
+ require 'state_machines/integrations/active_record'
@@ -0,0 +1,588 @@
1
+ require 'state_machines-activemodel'
2
+ require 'active_record'
3
+ require 'state_machines/integrations/active_record/version'
4
+
5
+ module StateMachines
6
+ module Integrations #:nodoc:
7
+ # Adds support for integrating state machines with ActiveRecord models.
8
+ #
9
+ # == Examples
10
+ #
11
+ # Below is an example of a simple state machine defined within an
12
+ # ActiveRecord model:
13
+ #
14
+ # class Vehicle < ActiveRecord::Base
15
+ # state_machine :initial => :parked do
16
+ # event :ignite do
17
+ # transition :parked => :idling
18
+ # end
19
+ # end
20
+ # end
21
+ #
22
+ # The examples in the sections below will use the above class as a
23
+ # reference.
24
+ #
25
+ # == Actions
26
+ #
27
+ # By default, the action that will be invoked when a state is transitioned
28
+ # is the +save+ action. This will cause the record to save the changes
29
+ # made to the state machine's attribute. *Note* that if any other changes
30
+ # were made to the record prior to transition, then those changes will
31
+ # be saved as well.
32
+ #
33
+ # For example,
34
+ #
35
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
36
+ # vehicle.name = 'Ford Explorer'
37
+ # vehicle.ignite # => true
38
+ # vehicle.reload # => #<Vehicle id: 1, name: "Ford Explorer", state: "idling">
39
+ #
40
+ # *Note* that if you want a transition to update additional attributes of the record,
41
+ # either the changes need to be made in a +before_transition+ callback or you need
42
+ # to save the record manually.
43
+ #
44
+ # == Events
45
+ #
46
+ # As described in StateMachines::InstanceMethods#state_machine, event
47
+ # attributes are created for every machine that allow transitions to be
48
+ # performed automatically when the object's action (in this case, :save)
49
+ # is called.
50
+ #
51
+ # In ActiveRecord, these automated events are run in the following order:
52
+ # * before validation - Run before callbacks and persist new states, then validate
53
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
54
+ # * after save - Run after callbacks
55
+ #
56
+ # For example,
57
+ #
58
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
59
+ # vehicle.state_event # => nil
60
+ # vehicle.state_event = 'invalid'
61
+ # vehicle.valid? # => false
62
+ # vehicle.errors.full_messages # => ["State event is invalid"]
63
+ #
64
+ # vehicle.state_event = 'ignite'
65
+ # vehicle.valid? # => true
66
+ # vehicle.save # => true
67
+ # vehicle.state # => "idling"
68
+ # vehicle.state_event # => nil
69
+ #
70
+ # Note that this can also be done on a mass-assignment basis:
71
+ #
72
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id: 1, name: nil, state: "idling">
73
+ # vehicle.state # => "idling"
74
+ #
75
+ # This technique is always used for transitioning states when the +save+
76
+ # action (which is the default) is configured for the machine.
77
+ #
78
+ # === Security implications
79
+ #
80
+ # Beware that public event attributes mean that events can be fired
81
+ # whenever mass-assignment is being used. If you want to prevent malicious
82
+ # users from tampering with events through URLs / forms, the attribute
83
+ # should be protected like so:
84
+ #
85
+ # class Vehicle < ActiveRecord::Base
86
+ # attr_protected :state_event
87
+ # # attr_accessible ... # Alternative technique
88
+ #
89
+ # state_machine do
90
+ # ...
91
+ # end
92
+ # end
93
+ #
94
+ # If you want to only have *some* events be able to fire via mass-assignment,
95
+ # you can build two state machines (one public and one protected) like so:
96
+ #
97
+ # class Vehicle < ActiveRecord::Base
98
+ # attr_protected :state_event # Prevent access to events in the first machine
99
+ #
100
+ # state_machine do
101
+ # # Define private events here
102
+ # end
103
+ #
104
+ # # Public machine targets the same state as the private machine
105
+ # state_machine :public_state, :attribute => :state do
106
+ # # Define public events here
107
+ # end
108
+ # end
109
+ #
110
+ # == Transactions
111
+ #
112
+ # In order to ensure that any changes made during transition callbacks
113
+ # are rolled back during a failed attempt, every transition is wrapped
114
+ # within a transaction.
115
+ #
116
+ # For example,
117
+ #
118
+ # class Message < ActiveRecord::Base
119
+ # end
120
+ #
121
+ # Vehicle.state_machine do
122
+ # before_transition do |vehicle, transition|
123
+ # Message.create(:content => transition.inspect)
124
+ # false
125
+ # end
126
+ # end
127
+ #
128
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
129
+ # vehicle.ignite # => false
130
+ # Message.count # => 0
131
+ #
132
+ # *Note* that only before callbacks that halt the callback chain and
133
+ # failed attempts to save the record will result in the transaction being
134
+ # rolled back. If an after callback halts the chain, the previous result
135
+ # still applies and the transaction is *not* rolled back.
136
+ #
137
+ # To turn off transactions:
138
+ #
139
+ # class Vehicle < ActiveRecord::Base
140
+ # state_machine :initial => :parked, :use_transactions => false do
141
+ # ...
142
+ # end
143
+ # end
144
+ #
145
+ # == Validations
146
+ #
147
+ # As mentioned in StateMachines::Machine#state, you can define behaviors,
148
+ # like validations, that only execute for certain states. One *important*
149
+ # caveat here is that, due to a constraint in ActiveRecord's validation
150
+ # framework, custom validators will not work as expected when defined to run
151
+ # in multiple states. For example:
152
+ #
153
+ # class Vehicle < ActiveRecord::Base
154
+ # state_machine do
155
+ # ...
156
+ # state :first_gear, :second_gear do
157
+ # validate :speed_is_legal
158
+ # end
159
+ # end
160
+ # end
161
+ #
162
+ # In this case, the <tt>:speed_is_legal</tt> validation will only get run
163
+ # for the <tt>:second_gear</tt> state. To avoid this, you can define your
164
+ # custom validation like so:
165
+ #
166
+ # class Vehicle < ActiveRecord::Base
167
+ # state_machine do
168
+ # ...
169
+ # state :first_gear, :second_gear do
170
+ # validate {|vehicle| vehicle.speed_is_legal}
171
+ # end
172
+ # end
173
+ # end
174
+ #
175
+ # == Validation errors
176
+ #
177
+ # If an event fails to successfully fire because there are no matching
178
+ # transitions for the current record, a validation error is added to the
179
+ # record's state attribute to help in determining why it failed and for
180
+ # reporting via the UI.
181
+ #
182
+ # For example,
183
+ #
184
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id: 1, name: nil, state: "idling">
185
+ # vehicle.ignite # => false
186
+ # vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
187
+ #
188
+ # If an event fails to fire because of a validation error on the record and
189
+ # *not* because a matching transition was not available, no error messages
190
+ # will be added to the state attribute.
191
+ #
192
+ # In addition, if you're using the <tt>ignite!</tt> version of the event,
193
+ # then the failure reason (such as the current validation errors) will be
194
+ # included in the exception that gets raised when the event fails. For
195
+ # example, assuming there's a validation on a field called +name+ on the class:
196
+ #
197
+ # vehicle = Vehicle.new
198
+ # vehicle.ignite! # => StateMachines::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
199
+ #
200
+ # == Scopes
201
+ #
202
+ # To assist in filtering models with specific states, a series of named
203
+ # scopes are defined on the model for finding records with or without a
204
+ # particular set of states.
205
+ #
206
+ # These named scopes are essentially the functional equivalent of the
207
+ # following definitions:
208
+ #
209
+ # class Vehicle < ActiveRecord::Base
210
+ # named_scope :with_states, lambda {|*states| {:conditions => {:state => states}}}
211
+ # # with_states also aliased to with_state
212
+ #
213
+ # named_scope :without_states, lambda {|*states| {:conditions => ['state NOT IN (?)', states]}}
214
+ # # without_states also aliased to without_state
215
+ # end
216
+ #
217
+ # *Note*, however, that the states are converted to their stored values
218
+ # before being passed into the query.
219
+ #
220
+ # Because of the way named scopes work in ActiveRecord, they can be
221
+ # chained like so:
222
+ #
223
+ # Vehicle.with_state(:parked).all(:order => 'id DESC')
224
+ #
225
+ # Note that states can also be referenced by the string version of their
226
+ # name:
227
+ #
228
+ # Vehicle.with_state('parked')
229
+ #
230
+ # == Callbacks
231
+ #
232
+ # All before/after transition callbacks defined for ActiveRecord models
233
+ # behave in the same way that other ActiveRecord callbacks behave. The
234
+ # object involved in the transition is passed in as an argument.
235
+ #
236
+ # For example,
237
+ #
238
+ # class Vehicle < ActiveRecord::Base
239
+ # state_machine :initial => :parked do
240
+ # before_transition any => :idling do |vehicle|
241
+ # vehicle.put_on_seatbelt
242
+ # end
243
+ #
244
+ # before_transition do |vehicle, transition|
245
+ # # log message
246
+ # end
247
+ #
248
+ # event :ignite do
249
+ # transition :parked => :idling
250
+ # end
251
+ # end
252
+ #
253
+ # def put_on_seatbelt
254
+ # ...
255
+ # end
256
+ # end
257
+ #
258
+ # Note, also, that the transition can be accessed by simply defining
259
+ # additional arguments in the callback block.
260
+ #
261
+ # === Failure callbacks
262
+ #
263
+ # +after_failure+ callbacks allow you to execute behaviors when a transition
264
+ # is allowed, but fails to save. This could be useful for something like
265
+ # auditing transition attempts. Since callbacks run within transactions in
266
+ # ActiveRecord, a save failure will cause any records that get created in
267
+ # your callback to roll back. You can work around this issue like so:
268
+ #
269
+ # class TransitionLog < ActiveRecord::Base
270
+ # establish_connection Rails.env.to_sym
271
+ # end
272
+ #
273
+ # class Vehicle < ActiveRecord::Base
274
+ # state_machine do
275
+ # after_failure do |vehicle, transition|
276
+ # TransitionLog.create(:vehicle => vehicle, :transition => transition)
277
+ # end
278
+ #
279
+ # ...
280
+ # end
281
+ # end
282
+ #
283
+ # The +TransitionLog+ model establishes a second connection to the database
284
+ # that allows new records to be saved without being affected by rollbacks
285
+ # in the +Vehicle+ model's transaction.
286
+ #
287
+ # === Callback Order
288
+ #
289
+ # Callbacks occur in the following order. Callbacks specific to state_machine
290
+ # are bolded. The remaining callbacks are part of ActiveRecord.
291
+ #
292
+ # * (-) save
293
+ # * (-) begin transaction (if enabled)
294
+ # * (1) *before_transition*
295
+ # * (-) valid
296
+ # * (2) before_validation
297
+ # * (-) validate
298
+ # * (3) after_validation
299
+ # * (4) before_save
300
+ # * (5) before_create
301
+ # * (-) create
302
+ # * (6) after_create
303
+ # * (7) after_save
304
+ # * (8) *after_transition*
305
+ # * (-) end transaction (if enabled)
306
+ # * (9) after_commit
307
+ #
308
+ # == Observers
309
+ #
310
+ # In addition to support for ActiveRecord-like hooks, there is additional
311
+ # support for ActiveRecord observers. Because of the way ActiveRecord
312
+ # observers are designed, there is less flexibility around the specific
313
+ # transitions that can be hooked in. However, a large number of hooks
314
+ # *are* supported. For example, if a transition for a record's +state+
315
+ # attribute changes the state from +parked+ to +idling+ via the +ignite+
316
+ # event, the following observer methods are supported:
317
+ # * before/after/after_failure_to-_ignite_from_parked_to_idling
318
+ # * before/after/after_failure_to-_ignite_from_parked
319
+ # * before/after/after_failure_to-_ignite_to_idling
320
+ # * before/after/after_failure_to-_ignite
321
+ # * before/after/after_failure_to-_transition_state_from_parked_to_idling
322
+ # * before/after/after_failure_to-_transition_state_from_parked
323
+ # * before/after/after_failure_to-_transition_state_to_idling
324
+ # * before/after/after_failure_to-_transition_state
325
+ # * before/after/after_failure_to-_transition
326
+ #
327
+ # The following class shows an example of some of these hooks:
328
+ #
329
+ # class VehicleObserver < ActiveRecord::Observer
330
+ # def before_save(vehicle)
331
+ # # log message
332
+ # end
333
+ #
334
+ # # Callback for :ignite event *before* the transition is performed
335
+ # def before_ignite(vehicle, transition)
336
+ # # log message
337
+ # end
338
+ #
339
+ # # Callback for :ignite event *after* the transition has been performed
340
+ # def after_ignite(vehicle, transition)
341
+ # # put on seatbelt
342
+ # end
343
+ #
344
+ # # Generic transition callback *before* the transition is performed
345
+ # def after_transition(vehicle, transition)
346
+ # Audit.log(vehicle, transition)
347
+ # end
348
+ # end
349
+ #
350
+ # More flexible transition callbacks can be defined directly within the
351
+ # model as described in StateMachines::Machine#before_transition
352
+ # and StateMachines::Machine#after_transition.
353
+ #
354
+ # To define a single observer for multiple state machines:
355
+ #
356
+ # class StateMachineObserver < ActiveRecord::Observer
357
+ # observe Vehicle, Switch, Project
358
+ #
359
+ # def after_transition(record, transition)
360
+ # Audit.log(record, transition)
361
+ # end
362
+ # end
363
+ #
364
+ # == Internationalization
365
+ #
366
+ # In Rails 2.2+, any error message that is generated from performing invalid
367
+ # transitions can be localized. The following default translations are used:
368
+ #
369
+ # en:
370
+ # activerecord:
371
+ # errors:
372
+ # messages:
373
+ # invalid: "is invalid"
374
+ # # %{value} = attribute value, %{state} = Human state name
375
+ # invalid_event: "cannot transition when %{state}"
376
+ # # %{value} = attribute value, %{event} = Human event name, %{state} = Human current state name
377
+ # invalid_transition: "cannot transition via %{event}"
378
+ #
379
+ # Notice that the interpolation syntax is %{key} in Rails 3+. In Rails 2.x,
380
+ # the appropriate syntax is {{key}}.
381
+ #
382
+ # You can override these for a specific model like so:
383
+ #
384
+ # en:
385
+ # activerecord:
386
+ # errors:
387
+ # models:
388
+ # user:
389
+ # invalid: "is not valid"
390
+ #
391
+ # In addition to the above, you can also provide translations for the
392
+ # various states / events in each state machine. Using the Vehicle example,
393
+ # state translations will be looked for using the following keys, where
394
+ # +model_name+ = "vehicle", +machine_name+ = "state" and +state_name+ = "parked":
395
+ # * <tt>activerecord.state_machines.#{model_name}.#{machine_name}.states.#{state_name}</tt>
396
+ # * <tt>activerecord.state_machines.#{model_name}.states.#{state_name}</tt>
397
+ # * <tt>activerecord.state_machines.#{machine_name}.states.#{state_name}</tt>
398
+ # * <tt>activerecord.state_machines.states.#{state_name}</tt>
399
+ #
400
+ # Event translations will be looked for using the following keys, where
401
+ # +model_name+ = "vehicle", +machine_name+ = "state" and +event_name+ = "ignite":
402
+ # * <tt>activerecord.state_machines.#{model_name}.#{machine_name}.events.#{event_name}</tt>
403
+ # * <tt>activerecord.state_machines.#{model_name}.events.#{event_name}</tt>
404
+ # * <tt>activerecord.state_machines.#{machine_name}.events.#{event_name}</tt>
405
+ # * <tt>activerecord.state_machines.events.#{event_name}</tt>
406
+ #
407
+ # An example translation configuration might look like so:
408
+ #
409
+ # es:
410
+ # activerecord:
411
+ # state_machines:
412
+ # states:
413
+ # parked: 'estacionado'
414
+ # events:
415
+ # park: 'estacionarse'
416
+ module ActiveRecord
417
+ include Base
418
+ include ActiveModel
419
+
420
+ # The default options to use for state machines using this integration
421
+ @defaults = {:action => :save}
422
+ class << self
423
+ # Classes that inherit from ActiveRecord::Base will automatically use
424
+ # the ActiveRecord integration.
425
+ def matching_ancestors
426
+ %w(ActiveRecord::Base)
427
+ end
428
+
429
+ def locale_path
430
+ "#{File.dirname(__FILE__)}/active_record/locale.rb"
431
+ end
432
+ end
433
+
434
+ protected
435
+
436
+ # Only runs validations on the action if using <tt>:save</tt>
437
+ def runs_validations_on_action?
438
+ action == :save
439
+ end
440
+
441
+ # Gets the db default for the machine's attribute
442
+ def owner_class_attribute_default
443
+ if owner_class.connected? && owner_class.table_exists?
444
+ if column = owner_class.columns_hash[attribute.to_s]
445
+ column.default
446
+ end
447
+ end
448
+ end
449
+
450
+ # Defines an initialization hook into the owner class for setting the
451
+ # initial state of the machine *before* any attributes are set on the
452
+ # object
453
+ def define_state_initializer
454
+ define_static_state_initializer
455
+ define_dynamic_state_initializer
456
+ end
457
+
458
+ # Initializes static states
459
+ def define_static_state_initializer
460
+ # This is the only available hook where the default set of attributes
461
+ # can be overridden for a new object *prior* to the processing of the
462
+ # attributes passed into #initialize
463
+ define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
464
+ if ActiveRecord.gem_version >= Gem::Version.new('4.2')
465
+ def _default_attributes #:nodoc:
466
+ result = super
467
+ self.state_machines.initialize_states(nil, :static => :force, :dynamic => false, :to => result)
468
+ result
469
+ end
470
+ else
471
+ def column_defaults(*) #:nodoc:
472
+ result = super
473
+ # No need to pass in an object, since the overrides will be forced
474
+ self.state_machines.initialize_states(nil, :static => :force, :dynamic => false, :to => result)
475
+ result
476
+ end
477
+ end
478
+ end_eval
479
+ end
480
+
481
+ # Initializes dynamic states
482
+ def define_dynamic_state_initializer
483
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
484
+ def initialize(*)
485
+ super do |*args|
486
+ self.class.state_machines.initialize_states(self)
487
+ yield(*args) if block_given?
488
+ end
489
+ end
490
+ end_eval
491
+ end
492
+
493
+ # Uses around callbacks to run state events if using the :save hook
494
+ def define_action_hook
495
+ if action_hook == :save
496
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
497
+ def save(*)
498
+ self.class.state_machine(#{name.inspect}).send(:around_save, self) { super }
499
+ end
500
+
501
+ def save!(*)
502
+ self.class.state_machine(#{name.inspect}).send(:around_save, self) { super } || raise(ActiveRecord::RecordInvalid.new(self))
503
+ end
504
+
505
+ def changed_for_autosave?
506
+ super || self.class.state_machines.any? {|name, machine| machine.action == :save && machine.read(self, :event)}
507
+ end
508
+ end_eval
509
+ else
510
+ super
511
+ end
512
+ end
513
+
514
+ # Runs state events around the machine's :save action
515
+ def around_save(object)
516
+ transaction(object) do
517
+ object.class.state_machines.transitions(object, action).perform { yield }
518
+ end
519
+ end
520
+
521
+ # Creates a scope for finding records *with* a particular state or
522
+ # states for the attribute
523
+ def create_with_scope(name)
524
+ create_scope(name, ->(values) { ["#{attribute_column} IN (?)", values] })
525
+ end
526
+
527
+ # Creates a scope for finding records *without* a particular state or
528
+ # states for the attribute
529
+ def create_without_scope(name)
530
+ create_scope(name, ->(values) { ["#{attribute_column} NOT IN (?)", values] })
531
+ end
532
+
533
+ # Generates the fully-qualifed column name for this machine's attribute
534
+ def attribute_column
535
+ connection = owner_class.connection
536
+ "#{connection.quote_table_name(owner_class.table_name)}.#{connection.quote_column_name(attribute)}"
537
+ end
538
+
539
+ # Runs a new database transaction, rolling back any changes by raising
540
+ # an ActiveRecord::Rollback exception if the yielded block fails
541
+ # (i.e. returns false).
542
+ def transaction(object)
543
+ result = nil
544
+ object.class.transaction do
545
+ raise ::ActiveRecord::Rollback unless result = yield
546
+ end
547
+ result
548
+ end
549
+
550
+ def locale_path
551
+ "#{File.dirname(__FILE__)}/active_record/locale.rb"
552
+ end
553
+
554
+ private
555
+
556
+ # Defines a new named scope with the given name
557
+ def create_scope(name, scope)
558
+ lambda { |model, values| model.where(scope.call(values)) }
559
+ end
560
+
561
+ # ActiveModel's use of method_missing / respond_to for attribute methods
562
+ # breaks both ancestor lookups and defined?(super). Need to special-case
563
+ # the existence of query attribute methods.
564
+ def owner_class_ancestor_has_method?(scope, method)
565
+ scope == :instance && method == "#{attribute}?" ? owner_class : super
566
+ end
567
+ end
568
+ end
569
+ class Machine
570
+ # FIXME
571
+ def initialize_state(object, options = {})
572
+ state = initial_state(object)
573
+ if state && (options[:force] || initialize_state?(object))
574
+ value = state.value
575
+
576
+ if hash = options[:to]
577
+ if hash.is_a?(Hash)
578
+ hash[attribute.to_s] = value
579
+ else # in ActiveRecord 4.2 hash is an Activerecord::AttributeSet
580
+ hash.write_from_user(attribute.to_s, value)
581
+ end
582
+ else
583
+ write(object, :state, value)
584
+ end
585
+ end
586
+ end
587
+ end
588
+ end