aasm 5.0.6 → 5.2.0

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 (55) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +13 -13
  3. data/Appraisals +6 -10
  4. data/CHANGELOG.md +33 -0
  5. data/Gemfile +1 -1
  6. data/README.md +102 -13
  7. data/aasm.gemspec +1 -1
  8. data/gemfiles/norails.gemfile +1 -1
  9. data/gemfiles/rails_4.2.gemfile +1 -0
  10. data/gemfiles/rails_4.2_mongoid_5.gemfile +1 -0
  11. data/gemfiles/rails_5.0.gemfile +1 -0
  12. data/gemfiles/rails_5.1.gemfile +1 -0
  13. data/gemfiles/rails_5.2.gemfile +1 -0
  14. data/lib/aasm.rb +0 -2
  15. data/lib/aasm/base.rb +30 -11
  16. data/lib/aasm/configuration.rb +3 -0
  17. data/lib/aasm/core/event.rb +7 -2
  18. data/lib/aasm/core/state.rb +6 -5
  19. data/lib/aasm/core/transition.rb +1 -1
  20. data/lib/aasm/dsl_helper.rb +24 -22
  21. data/lib/aasm/instance_base.rb +12 -1
  22. data/lib/aasm/localizer.rb +13 -3
  23. data/lib/aasm/persistence/active_record_persistence.rb +18 -0
  24. data/lib/aasm/persistence/base.rb +13 -2
  25. data/lib/aasm/persistence/orm.rb +23 -19
  26. data/lib/aasm/rspec/transition_from.rb +5 -1
  27. data/lib/aasm/version.rb +1 -1
  28. data/spec/database.rb +10 -12
  29. data/spec/en.yml +0 -3
  30. data/spec/{en_deprecated_style.yml → localizer_test_model_deprecated_style.yml} +6 -3
  31. data/spec/localizer_test_model_new_style.yml +11 -0
  32. data/spec/models/active_record/active_record_callback.rb +93 -0
  33. data/spec/models/active_record/localizer_test_model.rb +11 -3
  34. data/spec/models/active_record/namespaced.rb +16 -0
  35. data/spec/models/active_record/timestamp_example.rb +16 -0
  36. data/spec/models/default_state.rb +1 -1
  37. data/spec/models/mongoid/timestamp_example_mongoid.rb +20 -0
  38. data/spec/models/simple_example.rb +6 -0
  39. data/spec/models/timestamps_example.rb +19 -0
  40. data/spec/models/timestamps_with_named_machine_example.rb +13 -0
  41. data/spec/spec_helper.rb +5 -0
  42. data/spec/unit/api_spec.rb +4 -0
  43. data/spec/unit/inspection_multiple_spec.rb +9 -5
  44. data/spec/unit/inspection_spec.rb +7 -3
  45. data/spec/unit/localizer_spec.rb +49 -18
  46. data/spec/unit/persistence/active_record_persistence_multiple_spec.rb +17 -0
  47. data/spec/unit/persistence/active_record_persistence_spec.rb +79 -0
  48. data/spec/unit/persistence/mongoid_persistence_spec.rb +12 -0
  49. data/spec/unit/rspec_matcher_spec.rb +3 -0
  50. data/spec/unit/simple_example_spec.rb +15 -0
  51. data/spec/unit/state_spec.rb +21 -5
  52. data/spec/unit/timestamps_spec.rb +32 -0
  53. metadata +26 -13
  54. data/callbacks.txt +0 -51
  55. data/gemfiles/rails_3.2.gemfile +0 -14
@@ -9,5 +9,6 @@ gem "sequel"
9
9
  gem "dynamoid", "~>2.2", platforms: :ruby
10
10
  gem "aws-sdk", "~>2", platforms: :ruby
11
11
  gem "redis-objects"
12
+ gem "after_commit_everywhere", "~> 1.0"
12
13
 
13
14
  gemspec path: "../"
data/lib/aasm.rb CHANGED
@@ -1,5 +1,3 @@
1
- require 'ostruct'
2
-
3
1
  require 'aasm/version'
4
2
  require 'aasm/errors'
5
3
  require 'aasm/configuration'
data/lib/aasm/base.rb CHANGED
@@ -37,6 +37,9 @@ module AASM
37
37
  # string for a specific lock type i.e. FOR UPDATE NOWAIT
38
38
  configure :requires_lock, false
39
39
 
40
+ # automatically set `"#{state_name}_at" = ::Time.now` on state changes
41
+ configure :timestamps, false
42
+
40
43
  # set to true to forbid direct assignment of aasm_state column (in ActiveRecord)
41
44
  configure :no_direct_assignment, false
42
45
 
@@ -51,19 +54,12 @@ module AASM
51
54
  # Configure a logger, with default being a Logger to STDERR
52
55
  configure :logger, Logger.new(STDERR)
53
56
 
57
+ # setup timestamp-setting callback if enabled
58
+ setup_timestamps(@name)
59
+
54
60
  # make sure to raise an error if no_direct_assignment is enabled
55
61
  # and attribute is directly assigned though
56
- aasm_name = @name
57
-
58
- if @state_machine.config.no_direct_assignment
59
- @klass.send(:define_method, "#{@state_machine.config.column}=") do |state_name|
60
- if self.class.aasm(:"#{aasm_name}").state_machine.config.no_direct_assignment
61
- raise AASM::NoDirectAssignmentError.new('direct assignment of AASM column has been disabled (see AASM configuration for this class)')
62
- else
63
- super(state_name)
64
- end
65
- end
66
- end
62
+ setup_no_direct_assignment(@name)
67
63
  end
68
64
 
69
65
  # This method is both a getter and a setter
@@ -267,5 +263,28 @@ module AASM
267
263
  end
268
264
  end
269
265
 
266
+ def setup_timestamps(aasm_name)
267
+ return unless @state_machine.config.timestamps
268
+
269
+ after_all_transitions do
270
+ if self.class.aasm(:"#{aasm_name}").state_machine.config.timestamps
271
+ ts_setter = "#{aasm(aasm_name).to_state}_at="
272
+ respond_to?(ts_setter) && send(ts_setter, ::Time.now)
273
+ end
274
+ end
275
+ end
276
+
277
+ def setup_no_direct_assignment(aasm_name)
278
+ return unless @state_machine.config.no_direct_assignment
279
+
280
+ @klass.send(:define_method, "#{@state_machine.config.column}=") do |state_name|
281
+ if self.class.aasm(:"#{aasm_name}").state_machine.config.no_direct_assignment
282
+ raise AASM::NoDirectAssignmentError.new('direct assignment of AASM column has been disabled (see AASM configuration for this class)')
283
+ else
284
+ super(state_name)
285
+ end
286
+ end
287
+ end
288
+
270
289
  end
271
290
  end
@@ -24,6 +24,9 @@ module AASM
24
24
  # for ActiveRecord: use pessimistic locking
25
25
  attr_accessor :requires_lock
26
26
 
27
+ # automatically set `"#{state_name}_at" = ::Time.now` on state changes
28
+ attr_accessor :timestamps
29
+
27
30
  # forbid direct assignment in aasm_state column (in ActiveRecord)
28
31
  attr_accessor :no_direct_assignment
29
32
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  module AASM::Core
4
4
  class Event
5
- include DslHelper
5
+ include AASM::DslHelper
6
6
 
7
- attr_reader :name, :state_machine, :options
7
+ attr_reader :name, :state_machine, :options, :default_display_name
8
8
 
9
9
  def initialize(name, state_machine, options = {}, &block)
10
10
  @name = name
@@ -13,6 +13,7 @@ module AASM::Core
13
13
  @valid_transitions = {}
14
14
  @guards = Array(options[:guard] || options[:guards] || options[:if])
15
15
  @unless = Array(options[:unless]) #TODO: This could use a better name
16
+ @default_display_name = name.to_s.gsub(/_/, ' ').capitalize
16
17
 
17
18
  # from aasm4
18
19
  @options = options # QUESTION: .dup ?
@@ -109,6 +110,10 @@ module AASM::Core
109
110
  transitions.flat_map(&:failures)
110
111
  end
111
112
 
113
+ def to_s
114
+ name.to_s
115
+ end
116
+
112
117
  private
113
118
 
114
119
  def attach_event_guards(definitions)
@@ -2,12 +2,13 @@
2
2
 
3
3
  module AASM::Core
4
4
  class State
5
- attr_reader :name, :state_machine, :options
5
+ attr_reader :name, :state_machine, :options, :default_display_name
6
6
 
7
7
  def initialize(name, klass, state_machine, options={})
8
8
  @name = name
9
9
  @klass = klass
10
10
  @state_machine = state_machine
11
+ @default_display_name = name.to_s.gsub(/_/, ' ').capitalize
11
12
  update(options)
12
13
  end
13
14
 
@@ -54,11 +55,11 @@ module AASM::Core
54
55
  end
55
56
 
56
57
  def display_name
57
- @display_name ||= begin
58
+ @display_name = begin
58
59
  if Module.const_defined?(:I18n)
59
60
  localized_name
60
61
  else
61
- name.to_s.gsub(/_/, ' ').capitalize
62
+ @default_display_name
62
63
  end
63
64
  end
64
65
  end
@@ -75,8 +76,8 @@ module AASM::Core
75
76
  private
76
77
 
77
78
  def update(options = {})
78
- if options.key?(:display) then
79
- @display_name = options.delete(:display)
79
+ if options.key?(:display)
80
+ @default_display_name = options.delete(:display)
80
81
  end
81
82
  @options = options
82
83
  self
@@ -2,7 +2,7 @@
2
2
 
3
3
  module AASM::Core
4
4
  class Transition
5
- include DslHelper
5
+ include AASM::DslHelper
6
6
 
7
7
  attr_reader :from, :to, :event, :opts, :failures
8
8
  alias_method :options, :opts
@@ -1,30 +1,32 @@
1
- module DslHelper
1
+ module AASM
2
+ module DslHelper
2
3
 
3
- class Proxy
4
- attr_accessor :options
4
+ class Proxy
5
+ attr_accessor :options
5
6
 
6
- def initialize(options, valid_keys, source)
7
- @valid_keys = valid_keys
8
- @source = source
7
+ def initialize(options, valid_keys, source)
8
+ @valid_keys = valid_keys
9
+ @source = source
9
10
 
10
- @options = options
11
- end
11
+ @options = options
12
+ end
12
13
 
13
- def method_missing(name, *args, &block)
14
- if @valid_keys.include?(name)
15
- options[name] = Array(options[name])
16
- options[name] << block if block
17
- options[name] += Array(args)
18
- else
19
- @source.send name, *args, &block
14
+ def method_missing(name, *args, &block)
15
+ if @valid_keys.include?(name)
16
+ options[name] = Array(options[name])
17
+ options[name] << block if block
18
+ options[name] += Array(args)
19
+ else
20
+ @source.send name, *args, &block
21
+ end
20
22
  end
21
23
  end
22
- end
23
24
 
24
- def add_options_from_dsl(options, valid_keys, &block)
25
- proxy = Proxy.new(options, valid_keys, self)
26
- proxy.instance_eval(&block)
27
- proxy.options
28
- end
25
+ def add_options_from_dsl(options, valid_keys, &block)
26
+ proxy = Proxy.new(options, valid_keys, self)
27
+ proxy.instance_eval(&block)
28
+ proxy.options
29
+ end
29
30
 
30
- end
31
+ end
32
+ end
@@ -28,7 +28,7 @@ module AASM
28
28
  end
29
29
 
30
30
  def human_state
31
- AASM::Localizer.new.human_state_name(@instance.class, state_object_for_name(current_state))
31
+ state_object_for_name(current_state).display_name
32
32
  end
33
33
 
34
34
  def states(options={}, *args)
@@ -78,6 +78,17 @@ module AASM
78
78
  events
79
79
  end
80
80
 
81
+ def permitted_transitions
82
+ events(permitted: true).flat_map do |event|
83
+ available_transitions = event.transitions_from_state(current_state)
84
+ allowed_transitions = available_transitions.select { |t| t.allowed?(@instance) }
85
+
86
+ allowed_transitions.map do |transition|
87
+ { event: event.name, state: transition.to }
88
+ end
89
+ end
90
+ end
91
+
81
92
  def state_object_for_name(name)
82
93
  obj = @instance.class.aasm(@name).states.find {|s| s.name == name}
83
94
  raise AASM::UndefinedState, "State :#{name} doesn't exist" if obj.nil?
@@ -5,7 +5,7 @@ module AASM
5
5
  list << :"#{i18n_scope(klass)}.events.#{i18n_klass(ancestor)}.#{event}"
6
6
  list
7
7
  end
8
- translate_queue(checklist) || I18n.translate(checklist.shift, :default => event.to_s.humanize)
8
+ translate_queue(checklist) || I18n.translate(checklist.shift, :default => default_display_name(event))
9
9
  end
10
10
 
11
11
  def human_state_name(klass, state)
@@ -14,7 +14,7 @@ module AASM
14
14
  list << item_for(klass, state, ancestor, :old_style => true)
15
15
  list
16
16
  end
17
- translate_queue(checklist) || I18n.translate(checklist.shift, :default => state.to_s.humanize)
17
+ translate_queue(checklist) || I18n.translate(checklist.shift, :default => default_display_name(state))
18
18
  end
19
19
 
20
20
  private
@@ -46,8 +46,18 @@ module AASM
46
46
  end
47
47
 
48
48
  def ancestors_list(klass)
49
+ has_active_record_base = defined?(::ActiveRecord::Base)
49
50
  klass.ancestors.select do |ancestor|
50
- ancestor.respond_to?(:model_name) unless ancestor.name == 'ActiveRecord::Base'
51
+ not_active_record_base = has_active_record_base ? (ancestor != ::ActiveRecord::Base) : true
52
+ ancestor.respond_to?(:model_name) && not_active_record_base
53
+ end
54
+ end
55
+
56
+ def default_display_name(object) # Can use better arguement name
57
+ if object.respond_to?(:default_display_name)
58
+ object.default_display_name
59
+ else
60
+ object.to_s.gsub(/_/, ' ').capitalize
51
61
  end
52
62
  end
53
63
  end
@@ -61,6 +61,24 @@ module AASM
61
61
 
62
62
  private
63
63
 
64
+ def aasm_execute_after_commit
65
+ begin
66
+ require 'after_commit_everywhere'
67
+ raise LoadError unless Gem::Version.new(::AfterCommitEverywhere::VERSION) >= Gem::Version.new('0.1.5')
68
+
69
+ self.extend ::AfterCommitEverywhere
70
+ after_commit do
71
+ yield
72
+ end
73
+ rescue LoadError
74
+ warn <<-MSG
75
+ [DEPRECATION] :after_commit AASM callback is not safe in terms of race conditions and redundant calls.
76
+ Please add `gem 'after_commit_everywhere', '~> 1.0'` to your Gemfile in order to fix that.
77
+ MSG
78
+ yield
79
+ end
80
+ end
81
+
64
82
  def aasm_raise_invalid_record
65
83
  raise ActiveRecord::RecordInvalid.new(self)
66
84
  end
@@ -59,7 +59,9 @@ module AASM
59
59
  # make sure to create a (named) scope for each state
60
60
  def state_with_scope(*args)
61
61
  names = state_without_scope(*args)
62
- names.each { |name| create_scope(name) if create_scope?(name) }
62
+ names.each do |name|
63
+ create_scopes(name)
64
+ end
63
65
  end
64
66
  alias_method :state_without_scope, :state
65
67
  alias_method :state, :state_with_scope
@@ -71,7 +73,16 @@ module AASM
71
73
  end
72
74
 
73
75
  def create_scope(name)
74
- @klass.aasm_create_scope(@name, name)
76
+ @klass.aasm_create_scope(@name, name) if create_scope?(name)
77
+ end
78
+
79
+ def create_scopes(name)
80
+ if namespace?
81
+ # Create default scopes even when namespace? for backward compatibility
82
+ namepaced_name = "#{namespace}_#{name}"
83
+ create_scope(namepaced_name)
84
+ end
85
+ create_scope(name)
75
86
  end
76
87
  end # Base
77
88
 
@@ -81,6 +81,10 @@ module AASM
81
81
  true
82
82
  end
83
83
 
84
+ def aasm_execute_after_commit
85
+ yield
86
+ end
87
+
84
88
  def aasm_write_state_attribute(state, name=:default)
85
89
  aasm_write_attribute(self.class.aasm(name).attribute_name, aasm_raw_attribute_value(state, name))
86
90
  end
@@ -116,32 +120,32 @@ module AASM
116
120
 
117
121
  # Returns true if event was fired successfully and transaction completed.
118
122
  def aasm_fire_event(state_machine_name, name, options, *args, &block)
119
- if aasm_supports_transactions? && options[:persist]
120
- event = self.class.aasm(state_machine_name).state_machine.events[name]
121
- event.fire_callbacks(:before_transaction, self, *args)
122
- event.fire_global_callbacks(:before_all_transactions, self, *args)
123
-
124
- begin
125
- success = if options[:persist] && use_transactions?(state_machine_name)
126
- aasm_transaction(requires_new?(state_machine_name), requires_lock?(state_machine_name)) do
127
- super
128
- end
129
- else
123
+ return super unless aasm_supports_transactions? && options[:persist]
124
+
125
+ event = self.class.aasm(state_machine_name).state_machine.events[name]
126
+ event.fire_callbacks(:before_transaction, self, *args)
127
+ event.fire_global_callbacks(:before_all_transactions, self, *args)
128
+
129
+ begin
130
+ success = if options[:persist] && use_transactions?(state_machine_name)
131
+ aasm_transaction(requires_new?(state_machine_name), requires_lock?(state_machine_name)) do
130
132
  super
131
133
  end
134
+ else
135
+ super
136
+ end
132
137
 
133
- if success
138
+ if success && !(event.options.keys & [:after_commit, :after_all_commits]).empty?
139
+ aasm_execute_after_commit do
134
140
  event.fire_callbacks(:after_commit, self, *args)
135
141
  event.fire_global_callbacks(:after_all_commits, self, *args)
136
142
  end
137
-
138
- success
139
- ensure
140
- event.fire_callbacks(:after_transaction, self, *args)
141
- event.fire_global_callbacks(:after_all_transactions, self, *args)
142
143
  end
143
- else
144
- super
144
+
145
+ success
146
+ ensure
147
+ event.fire_callbacks(:after_transaction, self, *args)
148
+ event.fire_global_callbacks(:after_all_transactions, self, *args)
145
149
  end
146
150
  end
147
151
 
@@ -2,7 +2,11 @@ RSpec::Matchers.define :transition_from do |from_state|
2
2
  match do |obj|
3
3
  @state_machine_name ||= :default
4
4
  obj.aasm(@state_machine_name).current_state = from_state.to_sym
5
- obj.send(@event, *@args) && obj.aasm(@state_machine_name).current_state == @to_state.to_sym
5
+ begin
6
+ obj.send(@event, *@args) && obj.aasm(@state_machine_name).current_state == @to_state.to_sym
7
+ rescue AASM::InvalidTransition
8
+ false
9
+ end
6
10
  end
7
11
 
8
12
  chain :on do |state_machine_name|
data/lib/aasm/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module AASM
2
- VERSION = "5.0.6"
2
+ VERSION = "5.2.0"
3
3
  end
data/spec/database.rb CHANGED
@@ -5,17 +5,10 @@ ActiveRecord::Migration.suppress_messages do
5
5
  end
6
6
  end
7
7
 
8
- ActiveRecord::Migration.create_table "simple_new_dsls", :force => true do |t|
9
- t.string "status"
10
- end
11
- ActiveRecord::Migration.create_table "multiple_simple_new_dsls", :force => true do |t|
12
- t.string "status"
13
- end
14
- ActiveRecord::Migration.create_table "implemented_abstract_class_dsls", :force => true do |t|
15
- t.string "status"
16
- end
17
- ActiveRecord::Migration.create_table "users", :force => true do |t|
18
- t.string "status"
8
+ %w(simple_new_dsls multiple_simple_new_dsls implemented_abstract_class_dsls users multiple_namespaceds).each do |table_name|
9
+ ActiveRecord::Migration.create_table table_name, :force => true do |t|
10
+ t.string "status"
11
+ end
19
12
  end
20
13
 
21
14
  ActiveRecord::Migration.create_table "complex_active_record_examples", :force => true do |t|
@@ -27,7 +20,7 @@ ActiveRecord::Migration.suppress_messages do
27
20
  t.string "status"
28
21
  end
29
22
 
30
- %w(validators multiple_validators workers invalid_persistors multiple_invalid_persistors silent_persistors multiple_silent_persistors).each do |table_name|
23
+ %w(validators multiple_validators workers invalid_persistors multiple_invalid_persistors silent_persistors multiple_silent_persistors active_record_callbacks).each do |table_name|
31
24
  ActiveRecord::Migration.create_table table_name, :force => true do |t|
32
25
  t.string "name"
33
26
  t.string "status"
@@ -56,4 +49,9 @@ ActiveRecord::Migration.suppress_messages do
56
49
  t.string "state"
57
50
  t.string "some_string"
58
51
  end
52
+
53
+ ActiveRecord::Migration.create_table "timestamp_examples", :force => true do |t|
54
+ t.string "aasm_state"
55
+ t.datetime "opened_at"
56
+ end
59
57
  end