aasm 4.0.8 → 4.5.2

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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +7 -2
  3. data/CHANGELOG.md +43 -2
  4. data/CODE_OF_CONDUCT.md +13 -0
  5. data/Gemfile +6 -3
  6. data/LICENSE +1 -1
  7. data/PLANNED_CHANGES.md +8 -4
  8. data/README.md +171 -14
  9. data/aasm.gemspec +1 -5
  10. data/gemfiles/rails_3.2.gemfile +3 -2
  11. data/gemfiles/rails_4.0.gemfile +2 -6
  12. data/gemfiles/rails_4.0_mongo_mapper.gemfile +14 -0
  13. data/gemfiles/rails_4.1.gemfile +2 -6
  14. data/gemfiles/rails_4.1_mongo_mapper.gemfile +14 -0
  15. data/gemfiles/rails_4.2.gemfile +1 -5
  16. data/gemfiles/rails_4.2_mongo_mapper.gemfile +14 -0
  17. data/gemfiles/rails_4.2_mongoid_5.gemfile +12 -0
  18. data/lib/aasm/aasm.rb +69 -36
  19. data/lib/aasm/base.rb +51 -21
  20. data/lib/aasm/core/event.rb +7 -6
  21. data/lib/aasm/core/state.rb +11 -8
  22. data/lib/aasm/core/transition.rb +7 -5
  23. data/lib/aasm/errors.rb +15 -1
  24. data/lib/aasm/instance_base.rb +17 -13
  25. data/lib/aasm/localizer.rb +1 -1
  26. data/lib/aasm/persistence/active_record_persistence.rb +63 -73
  27. data/lib/aasm/persistence/base.rb +55 -20
  28. data/lib/aasm/persistence/mongo_mapper_persistence.rb +157 -0
  29. data/lib/aasm/persistence/mongoid_persistence.rb +16 -41
  30. data/lib/aasm/persistence/plain_persistence.rb +8 -7
  31. data/lib/aasm/persistence/sequel_persistence.rb +12 -9
  32. data/lib/aasm/persistence.rb +2 -0
  33. data/lib/aasm/rspec/allow_event.rb +22 -0
  34. data/lib/aasm/rspec/allow_transition_to.rb +22 -0
  35. data/lib/aasm/rspec/have_state.rb +22 -0
  36. data/lib/aasm/rspec/transition_from.rb +32 -0
  37. data/lib/aasm/rspec.rb +5 -0
  38. data/lib/aasm/state_machine.rb +20 -6
  39. data/lib/aasm/version.rb +1 -1
  40. data/spec/database.rb +27 -1
  41. data/spec/models/active_record/basic_active_record_two_state_machines_example.rb +25 -0
  42. data/spec/models/active_record/complex_active_record_example.rb +33 -0
  43. data/spec/models/active_record/derivate_new_dsl.rb +7 -0
  44. data/spec/models/active_record/false_state.rb +35 -0
  45. data/spec/models/active_record/gate.rb +39 -0
  46. data/spec/models/active_record/localizer_test_model.rb +34 -0
  47. data/spec/models/active_record/no_direct_assignment.rb +21 -0
  48. data/spec/models/active_record/no_scope.rb +21 -0
  49. data/spec/models/active_record/persisted_state.rb +12 -0
  50. data/spec/models/active_record/provided_and_persisted_state.rb +24 -0
  51. data/spec/models/active_record/reader.rb +7 -0
  52. data/spec/models/active_record/readme_job.rb +21 -0
  53. data/spec/models/active_record/simple_new_dsl.rb +17 -0
  54. data/spec/models/active_record/thief.rb +29 -0
  55. data/spec/models/active_record/transient.rb +6 -0
  56. data/spec/models/active_record/with_enum.rb +39 -0
  57. data/spec/models/active_record/with_false_enum.rb +31 -0
  58. data/spec/models/active_record/with_true_enum.rb +39 -0
  59. data/spec/models/active_record/writer.rb +6 -0
  60. data/spec/models/basic_two_state_machines_example.rb +25 -0
  61. data/spec/models/callbacks/basic.rb +3 -0
  62. data/spec/models/callbacks/basic_multiple.rb +75 -0
  63. data/spec/models/callbacks/guard_within_block_multiple.rb +66 -0
  64. data/spec/models/callbacks/multiple_transitions_transition_guard_multiple.rb +65 -0
  65. data/spec/models/callbacks/private_method_multiple.rb +44 -0
  66. data/spec/models/callbacks/with_args.rb +9 -9
  67. data/spec/models/callbacks/with_args_multiple.rb +61 -0
  68. data/spec/models/callbacks/{with_state_args.rb → with_state_arg.rb} +1 -1
  69. data/spec/models/callbacks/with_state_arg_multiple.rb +26 -0
  70. data/spec/models/complex_example.rb +222 -0
  71. data/spec/models/conversation.rb +47 -1
  72. data/spec/models/default_state.rb +12 -0
  73. data/spec/models/foo.rb +57 -0
  74. data/spec/models/foo_callback_multiple.rb +45 -0
  75. data/spec/models/guardian.rb +10 -0
  76. data/spec/models/guardian_multiple.rb +48 -0
  77. data/spec/models/initial_state_proc.rb +31 -0
  78. data/spec/models/invalid_persistor.rb +15 -0
  79. data/spec/models/mongo_mapper/complex_mongo_mapper_example.rb +37 -0
  80. data/spec/models/mongo_mapper/no_scope_mongo_mapper.rb +21 -0
  81. data/spec/models/mongo_mapper/simple_mongo_mapper.rb +23 -0
  82. data/spec/models/mongo_mapper/simple_new_dsl_mongo_mapper.rb +25 -0
  83. data/spec/models/mongoid/complex_mongoid_example.rb +37 -0
  84. data/spec/models/mongoid/no_scope_mongoid.rb +11 -0
  85. data/spec/models/mongoid/simple_mongoid.rb +12 -0
  86. data/spec/models/mongoid/simple_new_dsl_mongoid.rb +14 -1
  87. data/spec/models/no_initial_state.rb +25 -0
  88. data/spec/models/parametrised_event.rb +1 -1
  89. data/spec/models/parametrised_event_multiple.rb +29 -0
  90. data/spec/models/provided_state.rb +24 -0
  91. data/spec/models/sequel/complex_sequel_example.rb +45 -0
  92. data/spec/models/sequel/sequel_multiple.rb +25 -0
  93. data/spec/models/sequel/sequel_simple.rb +25 -0
  94. data/spec/models/silencer.rb +5 -0
  95. data/spec/models/simple_example.rb +15 -0
  96. data/spec/models/simple_multiple_example.rb +30 -0
  97. data/spec/models/state_machine_with_failed_event.rb +12 -0
  98. data/spec/models/sub_class.rb +7 -0
  99. data/spec/models/sub_class_with_more_states.rb +18 -0
  100. data/spec/models/super_class.rb +46 -0
  101. data/spec/models/transactor.rb +27 -0
  102. data/spec/models/valid_state_name.rb +23 -0
  103. data/spec/models/validator.rb +47 -0
  104. data/spec/spec_helper.rb +4 -4
  105. data/spec/unit/api_spec.rb +6 -1
  106. data/spec/unit/basic_two_state_machines_example_spec.rb +10 -0
  107. data/spec/unit/callback_multiple_spec.rb +295 -0
  108. data/spec/unit/callbacks_spec.rb +3 -2
  109. data/spec/unit/complex_example_spec.rb +2 -2
  110. data/spec/unit/complex_multiple_example_spec.rb +99 -0
  111. data/spec/unit/edge_cases_spec.rb +16 -0
  112. data/spec/unit/event_multiple_spec.rb +73 -0
  113. data/spec/unit/event_naming_spec.rb +2 -15
  114. data/spec/unit/event_spec.rb +11 -6
  115. data/spec/unit/guard_multiple_spec.rb +60 -0
  116. data/spec/unit/guard_spec.rb +12 -0
  117. data/spec/unit/initial_state_multiple_spec.rb +15 -0
  118. data/spec/unit/initial_state_spec.rb +3 -18
  119. data/spec/unit/inspection_multiple_spec.rb +201 -0
  120. data/spec/unit/inspection_spec.rb +12 -7
  121. data/spec/unit/localizer_spec.rb +0 -38
  122. data/spec/unit/persistence/active_record_persistence_multiple_spec.rb +573 -0
  123. data/spec/unit/persistence/active_record_persistence_spec.rb +70 -13
  124. data/spec/unit/persistence/mongo_mapper_persistence_multiple_spec.rb +146 -0
  125. data/spec/unit/persistence/mongo_mapper_persistence_spec.rb +93 -0
  126. data/spec/unit/persistence/mongoid_persistence_multiple_spec.rb +127 -0
  127. data/spec/unit/persistence/mongoid_persistence_spec.rb +79 -0
  128. data/spec/unit/persistence/sequel_persistence_multiple_spec.rb +153 -0
  129. data/spec/unit/persistence/sequel_persistence_spec.rb +20 -23
  130. data/spec/unit/readme_spec.rb +42 -0
  131. data/spec/unit/reloading_spec.rb +1 -1
  132. data/spec/unit/rspec_matcher_spec.rb +79 -0
  133. data/spec/unit/simple_example_spec.rb +26 -42
  134. data/spec/unit/simple_multiple_example_spec.rb +63 -0
  135. data/spec/unit/state_spec.rb +3 -1
  136. data/spec/unit/subclassing_multiple_spec.rb +39 -0
  137. data/spec/unit/subclassing_spec.rb +10 -10
  138. data/spec/unit/transition_spec.rb +39 -25
  139. metadata +152 -23
  140. data/spec/models/active_record/api.rb +0 -75
  141. data/spec/models/argument.rb +0 -11
  142. data/spec/models/auth_machine.rb +0 -88
  143. data/spec/models/bar.rb +0 -15
  144. data/spec/models/father.rb +0 -21
  145. data/spec/models/persistence.rb +0 -164
  146. data/spec/models/son.rb +0 -3
  147. data/spec/unit/persistence/mongoid_persistance_spec.rb +0 -152
@@ -10,7 +10,7 @@ module AASM
10
10
  #
11
11
  # Adds
12
12
  #
13
- # before_validation :aasm_ensure_initial_state, :on => :create
13
+ # after_initialize :aasm_ensure_initial_state
14
14
  #
15
15
  # As a result, it doesn't matter when you define your methods - the following 2 are equivalent
16
16
  #
@@ -30,7 +30,6 @@ module AASM
30
30
  #
31
31
  def self.included(base)
32
32
  base.send(:include, AASM::Persistence::Base)
33
- base.extend AASM::Persistence::ActiveRecordPersistence::ClassMethods
34
33
  base.send(:include, AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
35
34
 
36
35
  base.after_initialize do
@@ -41,34 +40,6 @@ module AASM
41
40
  base.validate :aasm_validate_states
42
41
  end
43
42
 
44
- module ClassMethods
45
-
46
- def find_in_state(number, state, *args)
47
- with_state_scope state do
48
- find(number, *args)
49
- end
50
- end
51
-
52
- def count_in_state(state, *args)
53
- with_state_scope state do
54
- count(*args)
55
- end
56
- end
57
-
58
- def calculate_in_state(state, *args)
59
- with_state_scope state do
60
- calculate(*args)
61
- end
62
- end
63
-
64
- protected
65
- def with_state_scope(state)
66
- with_scope :find => {:conditions => ["#{table_name}.#{aasm_column} = ?", state.to_s]} do
67
- yield if block_given?
68
- end
69
- end
70
- end
71
-
72
43
  module InstanceMethods
73
44
 
74
45
  # Writes <tt>state</tt> to the state column and persists it to the database
@@ -80,22 +51,18 @@ module AASM
80
51
  # Foo.find(1).aasm.current_state # => :closed
81
52
  #
82
53
  # NOTE: intended to be called from an event
83
- def aasm_write_state(state)
84
- old_value = read_attribute(self.class.aasm.attribute_name)
85
- aasm_write_attribute state
54
+ def aasm_write_state(state, name=:default)
55
+ old_value = read_attribute(self.class.aasm(name).attribute_name)
56
+ aasm_write_attribute state, name
86
57
 
87
- success = if aasm_skipping_validations
88
- value = aasm_raw_attribute_value state
89
- self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm.attribute_name => value) == 1
58
+ success = if aasm_skipping_validations(name)
59
+ value = aasm_raw_attribute_value(state, name)
60
+ aasm_update_column(name, value)
90
61
  else
91
62
  self.save
92
63
  end
93
- unless success
94
- write_attribute(self.class.aasm.attribute_name, old_value)
95
- return false
96
- end
97
64
 
98
- true
65
+ success ? true : aasm_rollback(name, old_value)
99
66
  end
100
67
 
101
68
  # Writes <tt>state</tt> to the state column, but does not persist it to the database
@@ -110,46 +77,56 @@ module AASM
110
77
  # Foo.find(1).aasm.current_state # => :closed
111
78
  #
112
79
  # NOTE: intended to be called from an event
113
- def aasm_write_state_without_persistence(state)
114
- aasm_write_attribute state
80
+ def aasm_write_state_without_persistence(state, name=:default)
81
+ aasm_write_attribute(state, name)
115
82
  end
116
83
 
117
84
  private
118
- def aasm_enum
119
- case AASM::StateMachine[self.class].config.enum
85
+
86
+ def aasm_update_column(name, value)
87
+ self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm(name).attribute_name => value) == 1
88
+ end
89
+
90
+ def aasm_rollback(name, old_value)
91
+ write_attribute(self.class.aasm(name).attribute_name, old_value)
92
+ false
93
+ end
94
+
95
+ def aasm_enum(name=:default)
96
+ case AASM::StateMachine[self.class][name].config.enum
120
97
  when false then nil
121
- when true then aasm_guess_enum_method
122
- when nil then aasm_guess_enum_method if aasm_column_looks_like_enum
123
- else AASM::StateMachine[self.class].config.enum
98
+ when true then aasm_guess_enum_method(name)
99
+ when nil then aasm_guess_enum_method(name) if aasm_column_looks_like_enum(name)
100
+ else AASM::StateMachine[self.class][name].config.enum
124
101
  end
125
102
  end
126
103
 
127
- def aasm_column_looks_like_enum
128
- self.class.columns_hash[self.class.aasm.attribute_name.to_s].type == :integer
104
+ def aasm_column_looks_like_enum(name=:default)
105
+ self.class.columns_hash[self.class.aasm(name).attribute_name.to_s].type == :integer
129
106
  end
130
107
 
131
- def aasm_guess_enum_method
132
- self.class.aasm.attribute_name.to_s.pluralize.to_sym
108
+ def aasm_guess_enum_method(name=:default)
109
+ self.class.aasm(name).attribute_name.to_s.pluralize.to_sym
133
110
  end
134
111
 
135
- def aasm_skipping_validations
136
- AASM::StateMachine[self.class].config.skip_validation_on_save
112
+ def aasm_skipping_validations(state_machine_name)
113
+ AASM::StateMachine[self.class][state_machine_name].config.skip_validation_on_save
137
114
  end
138
115
 
139
- def aasm_write_attribute(state)
140
- write_attribute self.class.aasm.attribute_name, aasm_raw_attribute_value(state)
116
+ def aasm_write_attribute(state, name=:default)
117
+ write_attribute(self.class.aasm(name).attribute_name, aasm_raw_attribute_value(state, name))
141
118
  end
142
119
 
143
- def aasm_raw_attribute_value(state)
144
- if aasm_enum
145
- self.class.send(aasm_enum)[state]
120
+ def aasm_raw_attribute_value(state, name=:default)
121
+ if aasm_enum(name)
122
+ self.class.send(aasm_enum(name))[state]
146
123
  else
147
124
  state.to_s
148
125
  end
149
126
  end
150
127
 
151
128
  # Ensures that if the aasm_state column is nil and the record is new
152
- # that the initial state gets populated before validation on create
129
+ # then the initial state gets populated before validation on create
153
130
  #
154
131
  # foo = Foo.new
155
132
  # foo.aasm_state # => nil
@@ -164,35 +141,48 @@ module AASM
164
141
  # foo.aasm_state # => nil
165
142
  #
166
143
  def aasm_ensure_initial_state
167
- # checking via respond_to? does not work in Rails <= 3
168
- # if respond_to?(self.class.aasm.attribute_name) && send(self.class.aasm.attribute_name).blank? # Rails 4
169
- if attributes.key?(self.class.aasm.attribute_name.to_s) && send(self.class.aasm.attribute_name).blank?
170
- aasm.enter_initial_state
144
+ AASM::StateMachine[self.class].keys.each do |state_machine_name|
145
+ # checking via respond_to? does not work in Rails <= 3
146
+ # if respond_to?(self.class.aasm(state_machine_name).attribute_name) && send(self.class.aasm(state_machine_name).attribute_name).blank? # Rails 4
147
+ if aasm_column_is_blank?(state_machine_name)
148
+ aasm(state_machine_name).enter_initial_state
149
+ end
171
150
  end
172
151
  end
173
152
 
174
- def aasm_fire_event(name, options, *args, &block)
175
- success = options[:persist] ? self.class.transaction(:requires_new => requires_new?) { super } : super
153
+ def aasm_column_is_blank?(state_machine_name)
154
+ attribute_name = self.class.aasm(state_machine_name).attribute_name
155
+ attribute_names.include?(attribute_name.to_s) && send(attribute_name).blank?
156
+ end
157
+
158
+ def aasm_fire_event(state_machine_name, name, options, *args, &block)
159
+ success = options[:persist] ? self.class.transaction(:requires_new => requires_new?(state_machine_name)) { super } : super
176
160
 
177
161
  if success && options[:persist]
178
- event = self.class.aasm.state_machine.events[name]
179
- event.fire_callbacks(:after_commit, self)
162
+ event = self.class.aasm(state_machine_name).state_machine.events[name]
163
+ event.fire_callbacks(:after_commit, self, *args)
180
164
  end
181
165
 
182
166
  success
183
167
  end
184
168
 
185
- def requires_new?
186
- AASM::StateMachine[self.class].config.requires_new_transaction
169
+ def requires_new?(state_machine_name)
170
+ AASM::StateMachine[self.class][state_machine_name].config.requires_new_transaction
187
171
  end
188
172
 
189
173
  def aasm_validate_states
190
- unless AASM::StateMachine[self.class].config.skip_validation_on_save
191
- if aasm.current_state && !aasm.states.include?(aasm.current_state)
192
- self.errors.add(AASM::StateMachine[self.class].config.column , "is invalid")
174
+ AASM::StateMachine[self.class].keys.each do |state_machine_name|
175
+ unless aasm_skipping_validations(state_machine_name)
176
+ if aasm_invalid_state?(state_machine_name)
177
+ self.errors.add(AASM::StateMachine[self.class][state_machine_name].config.column , "is invalid")
178
+ end
193
179
  end
194
180
  end
195
181
  end
182
+
183
+ def aasm_invalid_state?(state_machine_name)
184
+ aasm(state_machine_name).current_state && !aasm(state_machine_name).states.include?(aasm(state_machine_name).current_state)
185
+ end
196
186
  end # InstanceMethods
197
187
 
198
188
  end
@@ -32,10 +32,10 @@ module AASM
32
32
  # NOTE: intended to be called from an event
33
33
  #
34
34
  # This allows for nil aasm states - be sure to add validation to your model
35
- def aasm_read_state
36
- state = send(self.class.aasm.attribute_name)
35
+ def aasm_read_state(name=:default)
36
+ state = send(self.class.aasm(name).attribute_name)
37
37
  if new_record?
38
- state.blank? ? aasm.determine_state_name(self.class.aasm.initial_state) : state.to_sym
38
+ state.blank? ? aasm(name).determine_state_name(self.class.aasm(name).initial_state) : state.to_sym
39
39
  else
40
40
  state.blank? ? nil : state.to_sym
41
41
  end
@@ -55,27 +55,62 @@ module AASM
55
55
  # make sure to create a (named) scope for each state
56
56
  def state_with_scope(name, *args)
57
57
  state_without_scope(name, *args)
58
- if AASM::StateMachine[@klass].config.create_scopes && !@klass.respond_to?(name)
59
- if @klass.ancestors.map {|klass| klass.to_s}.include?("ActiveRecord::Base")
58
+ create_scope(name) if create_scope?(name)
59
+ end
60
+ alias_method :state_without_scope, :state
61
+ alias_method :state, :state_with_scope
62
+
63
+ private
64
+
65
+ def create_scope?(name)
66
+ @state_machine.config.create_scopes && !@klass.respond_to?(name)
67
+ end
68
+
69
+ def create_scope(name)
70
+ if ancestors_include?("ActiveRecord::Base")
71
+ create_for_active_record(name)
72
+ elsif ancestors_include?("Mongoid::Document")
73
+ create_for_mongoid(name)
74
+ elsif ancestors_include?("MongoMapper::Document")
75
+ create_for_mongomapper(name)
76
+ end
77
+ end
60
78
 
61
- conditions = {"#{@klass.table_name}.#{@klass.aasm.attribute_name}" => name.to_s}
62
- if ActiveRecord::VERSION::MAJOR >= 3
63
- @klass.class_eval do
64
- scope name, lambda { where(conditions) }
65
- end
66
- else
67
- @klass.class_eval do
68
- named_scope name, :conditions => conditions
69
- end
70
- end
71
- elsif @klass.ancestors.map {|klass| klass.to_s}.include?("Mongoid::Document")
72
- scope_options = lambda { @klass.send(:where, {@klass.aasm.attribute_name.to_sym => name.to_s}) }
73
- @klass.send(:scope, name, scope_options)
79
+ def ancestors_include?(class_name)
80
+ @klass.ancestors.map { |klass| klass.to_s }.include?(class_name)
81
+ end
82
+
83
+ def create_for_active_record(name)
84
+ conditions = {
85
+ "#{@klass.table_name}.#{@klass.aasm(@name).attribute_name}" => name.to_s
86
+ }
87
+ if ActiveRecord::VERSION::MAJOR >= 3
88
+ @klass.class_eval do
89
+ scope name, lambda { where(conditions) }
90
+ end
91
+ else
92
+ @klass.class_eval do
93
+ named_scope name, :conditions => conditions
74
94
  end
75
95
  end
76
96
  end
77
- alias_method :state_without_scope, :state
78
- alias_method :state, :state_with_scope
97
+
98
+ def create_for_mongoid(name)
99
+ klass = @klass
100
+ state_machine_name = @name
101
+ scope_options = lambda {
102
+ klass.send(
103
+ :where,
104
+ { klass.aasm(state_machine_name).attribute_name.to_sym => name.to_s }
105
+ )
106
+ }
107
+ @klass.send(:scope, name, scope_options)
108
+ end
109
+
110
+ def create_for_mongomapper(name)
111
+ conditions = { @klass.aasm(@name).attribute_name.to_sym => name.to_s }
112
+ @klass.scope(name, lambda { @klass.where(conditions) })
113
+ end
79
114
  end # Base
80
115
 
81
116
  end # AASM
@@ -0,0 +1,157 @@
1
+ require_relative 'base'
2
+
3
+ module AASM
4
+ module Persistence
5
+ module MongoMapperPersistence
6
+ # This method:
7
+ #
8
+ # * extends the model with ClassMethods
9
+ # * includes InstanceMethods
10
+ #
11
+ # Adds
12
+ #
13
+ # before_validation :aasm_ensure_initial_state, :on => :create
14
+ #
15
+ # As a result, it doesn't matter when you define your methods - the following 2 are equivalent
16
+ #
17
+ # class Foo
18
+ # include MongoMapper::Document
19
+ # def aasm_write_state(state)
20
+ # "bar"
21
+ # end
22
+ # include AASM
23
+ # end
24
+ #
25
+ # class Foo < ActiveRecord::Base
26
+ # include MongoMapper::Document
27
+ # include AASM
28
+ # def aasm_write_state(state)
29
+ # "bar"
30
+ # end
31
+ # end
32
+ #
33
+ def self.included(base)
34
+ base.send(:include, AASM::Persistence::Base)
35
+ base.send(:include, AASM::Persistence::MongoMapperPersistence::InstanceMethods)
36
+
37
+ base.before_create :aasm_ensure_initial_state
38
+
39
+ # ensure state is in the list of states
40
+ base.validate :aasm_validate_states
41
+ end
42
+
43
+ module InstanceMethods
44
+
45
+ # Writes <tt>state</tt> to the state column and persists it to the database
46
+ #
47
+ # foo = Foo.find(1)
48
+ # foo.aasm.current_state # => :opened
49
+ # foo.close!
50
+ # foo.aasm.current_state # => :closed
51
+ # Foo.find(1).aasm.current_state # => :closed
52
+ #
53
+ # NOTE: intended to be called from an event
54
+ def aasm_write_state(state, name=:default)
55
+ old_value = read_attribute(self.class.aasm(name).attribute_name)
56
+ write_attribute(self.class.aasm(name).attribute_name, state)
57
+
58
+ success = if aasm_skipping_validations(name)
59
+ value = aasm_raw_attribute_value(state, name)
60
+ self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm(name).attribute_name => value) == 1
61
+ else
62
+ self.save
63
+ end
64
+ unless success
65
+ write_attribute(self.class.aasm(name).attribute_name, old_value)
66
+ return false
67
+ end
68
+
69
+ true
70
+ end
71
+
72
+ # Writes <tt>state</tt> to the state column, but does not persist it to the database
73
+ #
74
+ # foo = Foo.find(1)
75
+ # foo.aasm.current_state # => :opened
76
+ # foo.close
77
+ # foo.aasm.current_state # => :closed
78
+ # Foo.find(1).aasm.current_state # => :opened
79
+ # foo.save
80
+ # foo.aasm.current_state # => :closed
81
+ # Foo.find(1).aasm.current_state # => :closed
82
+ #
83
+ # NOTE: intended to be called from an event
84
+ def aasm_write_state_without_persistence(state, name=:default)
85
+ aasm_write_attribute(state, name)
86
+ end
87
+
88
+ private
89
+ def aasm_enum(name=:default)
90
+ case AASM::StateMachine[self.class][name].config.enum
91
+ when false then nil
92
+ when true then aasm_guess_enum_method(name)
93
+ when nil then aasm_guess_enum_method(name) if aasm_column_looks_like_enum(name)
94
+ else AASM::StateMachine[self.class][name].config.enum
95
+ end
96
+ end
97
+
98
+ def aasm_column_looks_like_enum(name)
99
+ self.class.keys[self.class.aasm(name).attribute_name.to_s].type == Integer
100
+ end
101
+
102
+ def aasm_guess_enum_method(name)
103
+ self.class.aasm(name).attribute_name.to_s.pluralize.to_sym
104
+ end
105
+
106
+ def aasm_skipping_validations(state_machine_name)
107
+ AASM::StateMachine[self.class][state_machine_name].config.skip_validation_on_save
108
+ end
109
+
110
+ def aasm_write_attribute(state, name=:default)
111
+ write_attribute self.class.aasm(name).attribute_name, aasm_raw_attribute_value(state, name)
112
+ end
113
+
114
+ def aasm_raw_attribute_value(state, name=:default)
115
+ if aasm_enum(name)
116
+ self.class.send(aasm_enum(name))[state]
117
+ else
118
+ state.to_s
119
+ end
120
+ end
121
+
122
+ # Ensures that if the aasm_state column is nil and the record is new
123
+ # that the initial state gets populated before validation on create
124
+ #
125
+ # foo = Foo.new
126
+ # foo.aasm_state # => nil
127
+ # foo.valid?
128
+ # foo.aasm_state # => "open" (where :open is the initial state)
129
+ #
130
+ #
131
+ # foo = Foo.find(:first)
132
+ # foo.aasm_state # => 1
133
+ # foo.aasm_state = nil
134
+ # foo.valid?
135
+ # foo.aasm_state # => nil
136
+ #
137
+ def aasm_ensure_initial_state
138
+ AASM::StateMachine[self.class].keys.each do |state_machine_name|
139
+ send("#{self.class.aasm(state_machine_name).attribute_name}=", aasm(state_machine_name).enter_initial_state.to_s) if send(self.class.aasm(state_machine_name).attribute_name).blank?
140
+ end
141
+ end
142
+
143
+ def aasm_validate_states
144
+ AASM::StateMachine[self.class].keys.each do |state_machine_name|
145
+ send("#{self.class.aasm(state_machine_name).attribute_name}=", aasm(state_machine_name).enter_initial_state.to_s) if send(self.class.aasm(state_machine_name).attribute_name).blank?
146
+ unless AASM::StateMachine[self.class][state_machine_name].config.skip_validation_on_save
147
+ if aasm(state_machine_name).current_state && !aasm(state_machine_name).states.include?(aasm(state_machine_name).current_state)
148
+ self.errors.add(AASM::StateMachine[self.class][state_machine_name].config.column , "is invalid")
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end # InstanceMethods
154
+
155
+ end
156
+ end # Persistence
157
+ end # AASM
@@ -32,36 +32,9 @@ module AASM
32
32
  #
33
33
  def self.included(base)
34
34
  base.send(:include, AASM::Persistence::Base)
35
- base.extend AASM::Persistence::MongoidPersistence::ClassMethods
36
35
  base.send(:include, AASM::Persistence::MongoidPersistence::InstanceMethods)
37
36
 
38
- # Mongoid's Validatable gem dependency goes not have a before_validation_on_xxx hook yet.
39
- # base.before_validation_on_create :aasm_ensure_initial_state
40
- base.before_validation :aasm_ensure_initial_state
41
- # ensure initial aasm state even when validations are skipped
42
- base.before_create :aasm_ensure_initial_state
43
- end
44
-
45
- module ClassMethods
46
-
47
- def find_in_state(number, state, *args)
48
- with_state_scope state do
49
- find(number, *args)
50
- end
51
- end
52
-
53
- def count_in_state(state, *args)
54
- with_state_scope state do
55
- count(*args)
56
- end
57
- end
58
-
59
- def with_state_scope(state)
60
- with_scope where(aasm.attribute_name.to_sym => state.to_s) do
61
- yield if block_given?
62
- end
63
- end
64
-
37
+ base.after_initialize :aasm_ensure_initial_state
65
38
  end
66
39
 
67
40
  module InstanceMethods
@@ -76,12 +49,12 @@ module AASM
76
49
  # Foo.find(1).aasm.current_state # => :closed
77
50
  #
78
51
  # NOTE: intended to be called from an event
79
- def aasm_write_state(state)
80
- old_value = read_attribute(self.class.aasm.attribute_name)
81
- write_attribute(self.class.aasm.attribute_name, state.to_s)
52
+ def aasm_write_state(state, name=:default)
53
+ old_value = read_attribute(self.class.aasm(name).attribute_name)
54
+ write_attribute(self.class.aasm(name).attribute_name, state.to_s)
82
55
 
83
56
  unless self.save(:validate => false)
84
- write_attribute(self.class.aasm.attribute_name, old_value)
57
+ write_attribute(self.class.aasm(name).attribute_name, old_value)
85
58
  return false
86
59
  end
87
60
 
@@ -100,8 +73,8 @@ module AASM
100
73
  # Foo.find(1).aasm.current_state # => :closed
101
74
  #
102
75
  # NOTE: intended to be called from an event
103
- def aasm_write_state_without_persistence(state)
104
- write_attribute(self.class.aasm.attribute_name, state.to_s)
76
+ def aasm_write_state_without_persistence(state, name=:default)
77
+ write_attribute(self.class.aasm(name).attribute_name, state.to_s)
105
78
  end
106
79
 
107
80
  private
@@ -122,16 +95,18 @@ module AASM
122
95
  # foo.aasm_state # => nil
123
96
  #
124
97
  def aasm_ensure_initial_state
125
- send("#{self.class.aasm.attribute_name}=", aasm.enter_initial_state.to_s) if send(self.class.aasm.attribute_name).blank?
98
+ AASM::StateMachine[self.class].keys.each do |state_machine_name|
99
+ send("#{self.class.aasm(state_machine_name).attribute_name}=", aasm(state_machine_name).enter_initial_state.to_s) if send(self.class.aasm(state_machine_name).attribute_name).blank?
100
+ end
126
101
  end
127
102
  end # InstanceMethods
128
103
 
129
- module NamedScopeMethods
130
- def aasm_state_with_named_scope name, options = {}
131
- aasm_state_without_named_scope name, options
132
- self.named_scope name, :conditions => { "#{table_name}.#{self.aasm.attribute_name}" => name.to_s} unless self.respond_to?(name)
133
- end
134
- end
104
+ # module NamedScopeMethods
105
+ # def aasm_state_with_named_scope name, options = {}
106
+ # aasm_state_without_named_scope name, options
107
+ # self.named_scope name, :conditions => { "#{table_name}.#{self.aasm.attribute_name}" => name.to_s} unless self.respond_to?(name)
108
+ # end
109
+ # end
135
110
  end
136
111
  end # Persistence
137
112
  end # AASM
@@ -2,21 +2,22 @@ module AASM
2
2
  module Persistence
3
3
  module PlainPersistence
4
4
 
5
- def aasm_read_state
6
- # all the following lines behave like @current_state ||= aasm.enter_initial_state
7
- current = aasm.instance_variable_get("@current_state")
5
+ # may be overwritten by persistence mixins
6
+ def aasm_read_state(name=:default)
7
+ # all the following lines behave like @current_state ||= aasm(name).enter_initial_state
8
+ current = aasm(name).instance_variable_get("@current_state_#{name}")
8
9
  return current if current
9
- aasm.instance_variable_set("@current_state", aasm.enter_initial_state)
10
+ aasm(name).instance_variable_set("@current_state_#{name}", aasm(name).enter_initial_state)
10
11
  end
11
12
 
12
13
  # may be overwritten by persistence mixins
13
- def aasm_write_state(new_state)
14
+ def aasm_write_state(new_state, name=:default)
14
15
  true
15
16
  end
16
17
 
17
18
  # may be overwritten by persistence mixins
18
- def aasm_write_state_without_persistence(new_state)
19
- true
19
+ def aasm_write_state_without_persistence(new_state, name=:default)
20
+ aasm(name).instance_variable_set("@current_state_#{name}", new_state)
20
21
  end
21
22
 
22
23
  end
@@ -44,10 +44,10 @@ module AASM
44
44
  # NOTE: intended to be called from an event
45
45
  #
46
46
  # This allows for nil aasm states - be sure to add validation to your model
47
- def aasm_read_state
48
- state = send(self.class.aasm.attribute_name)
47
+ def aasm_read_state(name=:default)
48
+ state = send(self.class.aasm(name).attribute_name)
49
49
  if new? && state.to_s.strip.empty?
50
- aasm.determine_state_name(self.class.aasm.initial_state)
50
+ aasm(name).determine_state_name(self.class.aasm(name).initial_state)
51
51
  elsif state.nil?
52
52
  nil
53
53
  else
@@ -71,8 +71,11 @@ module AASM
71
71
  # foo.aasm_state # => nil
72
72
  #
73
73
  def aasm_ensure_initial_state
74
- aasm.enter_initial_state if
75
- send(self.class.aasm.attribute_name).to_s.strip.empty?
74
+ AASM::StateMachine[self.class].keys.each do |state_machine_name|
75
+ aasm(state_machine_name).enter_initial_state if
76
+ (new? || values.key?(self.class.aasm(state_machine_name).attribute_name)) &&
77
+ send(self.class.aasm(state_machine_name).attribute_name).to_s.strip.empty?
78
+ end
76
79
  end
77
80
 
78
81
  # Writes <tt>state</tt> to the state column and persists it to the database
@@ -84,8 +87,8 @@ module AASM
84
87
  # Foo[1].aasm.current_state # => :closed
85
88
  #
86
89
  # NOTE: intended to be called from an event
87
- def aasm_write_state state
88
- aasm_column = self.class.aasm.attribute_name
90
+ def aasm_write_state state, name=:default
91
+ aasm_column = self.class.aasm(name).attribute_name
89
92
  update_only({aasm_column => state.to_s}, aasm_column)
90
93
  end
91
94
 
@@ -101,8 +104,8 @@ module AASM
101
104
  # Foo[1].aasm.current_state # => :closed
102
105
  #
103
106
  # NOTE: intended to be called from an event
104
- def aasm_write_state_without_persistence state
105
- send("#{self.class.aasm.attribute_name}=", state.to_s)
107
+ def aasm_write_state_without_persistence state, name=:default
108
+ send("#{self.class.aasm(name).attribute_name}=", state.to_s)
106
109
  end
107
110
  end
108
111
  end
@@ -10,6 +10,8 @@ module AASM
10
10
  include_persistence base, :active_record
11
11
  elsif hierarchy.include?("Mongoid::Document")
12
12
  include_persistence base, :mongoid
13
+ elsif hierarchy.include?("MongoMapper::Document")
14
+ include_persistence base, :mongo_mapper
13
15
  elsif hierarchy.include?("Sequel::Model")
14
16
  include_persistence base, :sequel
15
17
  else
@@ -0,0 +1,22 @@
1
+ RSpec::Matchers.define :allow_event do |event|
2
+ match do |obj|
3
+ @state_machine_name ||= :default
4
+ obj.aasm(@state_machine_name).may_fire_event?(event)
5
+ end
6
+
7
+ chain :on do |state_machine_name|
8
+ @state_machine_name = state_machine_name
9
+ end
10
+
11
+ description do
12
+ "allow event #{expected} (on :#{@state_machine_name})"
13
+ end
14
+
15
+ failure_message do |obj|
16
+ "expected that the event :#{expected} would be allowed (on :#{@state_machine_name})"
17
+ end
18
+
19
+ failure_message_when_negated do |obj|
20
+ "expected that the event :#{expected} would not be allowed (on :#{@state_machine_name})"
21
+ end
22
+ end