state_machines-activerecord 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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