alexrevin-aasm_numerical 2.3.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 (39) hide show
  1. data/.document +5 -0
  2. data/.gitignore +11 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +20 -0
  5. data/README.md +149 -0
  6. data/Rakefile +27 -0
  7. data/lib/alexrevin-aasm_numerical.rb +10 -0
  8. data/lib/alexrevin-aasm_numerical/aasm.rb +222 -0
  9. data/lib/alexrevin-aasm_numerical/event.rb +127 -0
  10. data/lib/alexrevin-aasm_numerical/localizer.rb +36 -0
  11. data/lib/alexrevin-aasm_numerical/persistence.rb +14 -0
  12. data/lib/alexrevin-aasm_numerical/persistence/active_record_persistence.rb +257 -0
  13. data/lib/alexrevin-aasm_numerical/state.rb +53 -0
  14. data/lib/alexrevin-aasm_numerical/state_machine.rb +31 -0
  15. data/lib/alexrevin-aasm_numerical/state_transition.rb +46 -0
  16. data/lib/alexrevin-aasm_numerical/supporting_classes.rb +6 -0
  17. data/lib/alexrevin-aasm_numerical/version.rb +3 -0
  18. data/spec/database.yml +3 -0
  19. data/spec/en.yml +10 -0
  20. data/spec/functional/conversation.rb +49 -0
  21. data/spec/functional/conversation_spec.rb +8 -0
  22. data/spec/schema.rb +7 -0
  23. data/spec/spec_helper.rb +16 -0
  24. data/spec/unit/aasm_spec.rb +462 -0
  25. data/spec/unit/active_record_persistence_spec.rb +246 -0
  26. data/spec/unit/before_after_callbacks_spec.rb +79 -0
  27. data/spec/unit/event_spec.rb +140 -0
  28. data/spec/unit/localizer_spec.rb +51 -0
  29. data/spec/unit/state_spec.rb +85 -0
  30. data/spec/unit/state_transition_spec.rb +163 -0
  31. data/test/functional/auth_machine_test.rb +148 -0
  32. data/test/models/process.rb +18 -0
  33. data/test/test_helper.rb +43 -0
  34. data/test/unit/aasm_test.rb +0 -0
  35. data/test/unit/event_test.rb +54 -0
  36. data/test/unit/state_machine_test.rb +37 -0
  37. data/test/unit/state_test.rb +69 -0
  38. data/test/unit/state_transition_test.rb +75 -0
  39. metadata +254 -0
@@ -0,0 +1,127 @@
1
+ class AASM::SupportingClasses::Event
2
+ attr_reader :name, :success, :options
3
+
4
+ def initialize(name, options = {}, &block)
5
+ @name = name
6
+ @transitions = []
7
+ update(options, &block)
8
+ end
9
+
10
+ # a neutered version of fire - it doesn't actually fir the event, it just
11
+ # executes the transition guards to determine if a transition is even
12
+ # an option given current conditions.
13
+ def may_fire?(obj, to_state=nil)
14
+ transitions = @transitions.select { |t| t.from == obj.aasm_current_state }
15
+ return false if transitions.size == 0
16
+
17
+ result = false
18
+ transitions.each do |transition|
19
+ next if to_state and !Array(transition.to).include?(to_state)
20
+ if transition.perform(obj)
21
+ result = true
22
+ break
23
+ end
24
+ end
25
+ result
26
+ end
27
+
28
+ def fire(obj, to_state=nil, *args)
29
+ transitions = @transitions.select { |t| t.from == obj.aasm_current_state }
30
+ raise AASM::InvalidTransition, "Event '#{name}' cannot transition from '#{obj.aasm_current_state}'" if transitions.size == 0
31
+
32
+ next_state = nil
33
+ transitions.each do |transition|
34
+ next if to_state and !Array(transition.to).include?(to_state)
35
+ if transition.perform(obj, *args)
36
+ next_state = to_state || Array(transition.to).first
37
+ transition.execute(obj, *args)
38
+ break
39
+ end
40
+ end
41
+ next_state
42
+ end
43
+
44
+ def transitions_from_state?(state)
45
+ @transitions.any? { |t| t.from == state }
46
+ end
47
+
48
+ def transitions_from_state(state)
49
+ @transitions.select { |t| t.from == state }
50
+ end
51
+
52
+ def all_transitions
53
+ @transitions
54
+ end
55
+
56
+ def call_action(action, record)
57
+ action = @options[action]
58
+ action.is_a?(Array) ?
59
+ action.each {|a| _call_action(a, record)} :
60
+ _call_action(action, record)
61
+ end
62
+
63
+ def ==(event)
64
+ if event.is_a? Symbol
65
+ name == event
66
+ else
67
+ name == event.name
68
+ end
69
+ end
70
+
71
+ def update(options = {}, &block)
72
+ if options.key?(:success) then
73
+ @success = options[:success]
74
+ end
75
+ if options.key?(:error) then
76
+ @error = options[:error]
77
+ end
78
+ if block then
79
+ instance_eval(&block)
80
+ end
81
+ @options = options
82
+ self
83
+ end
84
+
85
+ def execute_success_callback(obj, success = nil)
86
+ callback = success || @success
87
+ case(callback)
88
+ when String, Symbol
89
+ obj.send(callback)
90
+ when Proc
91
+ callback.call(obj)
92
+ when Array
93
+ callback.each{|meth|self.execute_success_callback(obj, meth)}
94
+ end
95
+ end
96
+
97
+ def execute_error_callback(obj, error, error_callback=nil)
98
+ callback = error_callback || @error
99
+ raise error unless callback
100
+ case(callback)
101
+ when String, Symbol
102
+ raise NoMethodError unless obj.respond_to?(callback.to_sym)
103
+ obj.send(callback, error)
104
+ when Proc
105
+ callback.call(obj, error)
106
+ when Array
107
+ callback.each{|meth|self.execute_error_callback(obj, error, meth)}
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def _call_action(action, record)
114
+ case action
115
+ when Symbol, String
116
+ record.send(action)
117
+ when Proc
118
+ action.call(record)
119
+ end
120
+ end
121
+
122
+ def transitions(trans_opts)
123
+ Array(trans_opts[:from]).each do |s|
124
+ @transitions << AASM::SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,36 @@
1
+ class AASM::Localizer
2
+ def human_event_name(klass, event)
3
+ defaults = ancestors_list(klass).map do |ancestor|
4
+ :"#{i18n_scope(klass)}.events.#{i18n_klass(ancestor)}.#{event}"
5
+ end << event.to_s.humanize
6
+
7
+ I18n.translate(defaults.shift, :default => defaults, :raise => true)
8
+ end
9
+
10
+ def human_state(obj)
11
+ klass = obj.class
12
+ defaults = ancestors_list(klass).map do |ancestor|
13
+ :"#{i18n_scope(klass)}.attributes.#{i18n_klass(ancestor)}.#{klass.aasm_column}.#{obj.aasm_current_state}"
14
+ end << obj.aasm_current_state.to_s.humanize
15
+
16
+ I18n.translate(defaults.shift, :default => defaults, :raise => true)
17
+ end
18
+
19
+ private
20
+
21
+ # added for rails 2.x compatibility
22
+ def i18n_scope(klass)
23
+ klass.respond_to?(:i18n_scope) ? klass.i18n_scope : :activerecord
24
+ end
25
+
26
+ # added for rails < 3.0.3 compatibility
27
+ def i18n_klass(klass)
28
+ klass.model_name.respond_to?(:i18n_key) ? klass.model_name.i18n_key : klass.name.underscore
29
+ end
30
+
31
+ def ancestors_list(klass)
32
+ klass.ancestors.select do |ancestor|
33
+ ancestor.respond_to?(:model_name) unless ancestor == ActiveRecord::Base
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ module AASM::Persistence
2
+
3
+ # Checks to see this class or any of it's superclasses inherit from
4
+ # ActiveRecord::Base and if so includes ActiveRecordPersistence
5
+ def self.set_persistence(base)
6
+ # Use a fancier auto-loading thingy, perhaps. When there are more persistence engines.
7
+ hierarchy = base.ancestors.map {|klass| klass.to_s}
8
+
9
+ if hierarchy.include?("ActiveRecord::Base")
10
+ require File.join(File.dirname(__FILE__), 'persistence', 'active_record_persistence')
11
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,257 @@
1
+ module AASM
2
+ module Persistence
3
+ module ActiveRecordPersistence
4
+ # This method:
5
+ #
6
+ # * extends the model with ClassMethods
7
+ # * includes InstanceMethods
8
+ #
9
+ # Unless the corresponding methods are already defined, it includes
10
+ # * ReadState
11
+ # * WriteState
12
+ # * WriteStateWithoutPersistence
13
+ #
14
+ # Adds
15
+ #
16
+ # before_validation :aasm_ensure_initial_state, :on => :create
17
+ #
18
+ # As a result, it doesn't matter when you define your methods - the following 2 are equivalent
19
+ #
20
+ # class Foo < ActiveRecord::Base
21
+ # def aasm_write_state(state)
22
+ # "bar"
23
+ # end
24
+ # include AASM
25
+ # end
26
+ #
27
+ # class Foo < ActiveRecord::Base
28
+ # include AASM
29
+ # def aasm_write_state(state)
30
+ # "bar"
31
+ # end
32
+ # end
33
+ #
34
+ def self.included(base)
35
+ base.extend AASM::Persistence::ActiveRecordPersistence::ClassMethods
36
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
37
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence::ReadState) unless base.method_defined?(:aasm_read_state)
38
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence::WriteState) unless base.method_defined?(:aasm_write_state)
39
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence) unless base.method_defined?(:aasm_write_state_without_persistence)
40
+
41
+ if base.respond_to?(:named_scope) || base.respond_to?(:scope)
42
+ base.extend(AASM::Persistence::ActiveRecordPersistence::NamedScopeMethods)
43
+
44
+ base.class_eval do
45
+ class << self
46
+ unless method_defined?(:aasm_state_without_scope)
47
+ alias_method :aasm_state_without_scope, :aasm_state
48
+ alias_method :aasm_state, :aasm_state_with_scope
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ if ActiveRecord::VERSION::MAJOR >= 3
55
+ base.before_validation(:aasm_ensure_initial_state, :on => :create)
56
+ else
57
+ base.before_validation_on_create(:aasm_ensure_initial_state)
58
+ end
59
+ end
60
+
61
+ module ClassMethods
62
+ # Maps to the aasm_column in the database. Defaults to "aasm_state". You can write:
63
+ #
64
+ # create_table :foos do |t|
65
+ # t.string :name
66
+ # t.string :aasm_state
67
+ # end
68
+ #
69
+ # class Foo < ActiveRecord::Base
70
+ # include AASM
71
+ # end
72
+ #
73
+ # OR:
74
+ #
75
+ # create_table :foos do |t|
76
+ # t.string :name
77
+ # t.string :status
78
+ # end
79
+ #
80
+ # class Foo < ActiveRecord::Base
81
+ # include AASM
82
+ # aasm_column :status
83
+ # end
84
+ #
85
+ # This method is both a getter and a setter
86
+ def aasm_column(column_name=nil)
87
+ if column_name
88
+ AASM::StateMachine[self].config.column = column_name.to_sym
89
+ # @aasm_column = column_name.to_sym
90
+ else
91
+ AASM::StateMachine[self].config.column ||= :aasm_state
92
+ # @aasm_column ||= :aasm_state
93
+ end
94
+ # @aasm_column
95
+ AASM::StateMachine[self].config.column
96
+ end
97
+
98
+ def find_in_state(number, state, *args)
99
+ with_state_scope state do
100
+ find(number, *args)
101
+ end
102
+ end
103
+
104
+ def count_in_state(state, *args)
105
+ with_state_scope state do
106
+ count(*args)
107
+ end
108
+ end
109
+
110
+ def calculate_in_state(state, *args)
111
+ with_state_scope state do
112
+ calculate(*args)
113
+ end
114
+ end
115
+
116
+ protected
117
+ def with_state_scope(state)
118
+ with_scope :find => {:conditions => ["#{table_name}.#{aasm_column} = ?", state.to_s]} do
119
+ yield if block_given?
120
+ end
121
+ end
122
+ end
123
+
124
+ module InstanceMethods
125
+
126
+ # Returns the current aasm_state of the object. Respects reload and
127
+ # any changes made to the aasm_state field directly
128
+ #
129
+ # Internally just calls <tt>aasm_read_state</tt>
130
+ #
131
+ # foo = Foo.find(1)
132
+ # foo.aasm_current_state # => :pending
133
+ # foo.aasm_state = "opened"
134
+ # foo.aasm_current_state # => :opened
135
+ # foo.close # => calls aasm_write_state_without_persistence
136
+ # foo.aasm_current_state # => :closed
137
+ # foo.reload
138
+ # foo.aasm_current_state # => :pending
139
+ #
140
+ def aasm_current_state
141
+ @current_state = aasm_read_state
142
+ end
143
+
144
+ private
145
+
146
+ # Ensures that if the aasm_state column is nil and the record is new
147
+ # that the initial state gets populated before validation on create
148
+ #
149
+ # foo = Foo.new
150
+ # foo.aasm_state # => nil
151
+ # foo.valid?
152
+ # foo.aasm_state # => "open" (where :open is the initial state)
153
+ #
154
+ #
155
+ # foo = Foo.find(:first)
156
+ # foo.aasm_state # => 1
157
+ # foo.aasm_state = nil
158
+ # foo.valid?
159
+ # foo.aasm_state # => nil
160
+ #
161
+ def aasm_ensure_initial_state
162
+ send("#{self.class.aasm_column}=", self.aasm_enter_initial_state.to_s) if send(self.class.aasm_column).blank?
163
+ end
164
+
165
+ end
166
+
167
+ module WriteStateWithoutPersistence
168
+ # Writes <tt>state</tt> to the state column, but does not persist it to the database
169
+ #
170
+ # foo = Foo.find(1)
171
+ # foo.aasm_current_state # => :opened
172
+ # foo.close
173
+ # foo.aasm_current_state # => :closed
174
+ # Foo.find(1).aasm_current_state # => :opened
175
+ # foo.save
176
+ # foo.aasm_current_state # => :closed
177
+ # Foo.find(1).aasm_current_state # => :closed
178
+ #
179
+ # NOTE: intended to be called from an event
180
+ def aasm_write_state_without_persistence(state)
181
+ write_attribute(self.class.aasm_column, state.to_s)
182
+ end
183
+ end
184
+
185
+ module WriteState
186
+ # Writes <tt>state</tt> to the state column and persists it to the database
187
+ #
188
+ # foo = Foo.find(1)
189
+ # foo.aasm_current_state # => :opened
190
+ # foo.close!
191
+ # foo.aasm_current_state # => :closed
192
+ # Foo.find(1).aasm_current_state # => :closed
193
+ #
194
+ # NOTE: intended to be called from an event
195
+ def aasm_write_state(state)
196
+ old_value = read_attribute(self.class.aasm_column)
197
+ write_attribute(self.class.aasm_column, state.to_s)
198
+
199
+ unless self.save
200
+ write_attribute(self.class.aasm_column, old_value)
201
+ return false
202
+ end
203
+
204
+ true
205
+ end
206
+ end
207
+
208
+ module ReadState
209
+
210
+ # Returns the value of the aasm_column - called from <tt>aasm_current_state</tt>
211
+ #
212
+ # If it's a new record, and the aasm state column is blank it returns the initial state:
213
+ #
214
+ # class Foo < ActiveRecord::Base
215
+ # include AASM
216
+ # aasm_column :status
217
+ # aasm_state :opened
218
+ # aasm_state :closed
219
+ # end
220
+ #
221
+ # foo = Foo.new
222
+ # foo.current_state # => :opened
223
+ # foo.close
224
+ # foo.current_state # => :closed
225
+ #
226
+ # foo = Foo.find(1)
227
+ # foo.current_state # => :opened
228
+ # foo.aasm_state = nil
229
+ # foo.current_state # => nil
230
+ #
231
+ # NOTE: intended to be called from an event
232
+ #
233
+ # This allows for nil aasm states - be sure to add validation to your model
234
+ def aasm_read_state
235
+ if new_record?
236
+ send(self.class.aasm_column).blank? ? aasm_determine_state_name(self.class.aasm_initial_state) : send(self.class.aasm_column).to_sym
237
+ else
238
+ send(self.class.aasm_column).nil? ? nil : send(self.class.aasm_column).to_sym
239
+ end
240
+ end
241
+ end
242
+
243
+ module NamedScopeMethods
244
+ def aasm_state_with_scope name, options = {}
245
+ aasm_state_without_scope name, options
246
+
247
+ unless self.respond_to?(name)
248
+ scope_options = {:conditions => { "#{table_name}.#{self.aasm_column}" => name.to_s}}
249
+ scope_method = ActiveRecord::VERSION::MAJOR >= 3 ? :scope : :named_scope
250
+ self.send(scope_method, name, scope_options)
251
+ end
252
+
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,53 @@
1
+ class AASM::SupportingClasses::State
2
+ attr_reader :name, :options
3
+
4
+ def initialize(name, options={})
5
+ @name = name
6
+ update(options)
7
+ end
8
+
9
+ def ==(state)
10
+ if state.is_a? Symbol
11
+ name == state
12
+ else
13
+ name == state.name
14
+ end
15
+ end
16
+
17
+ def call_action(action, record)
18
+ action = @options[action]
19
+ catch :halt_aasm_chain do
20
+ action.is_a?(Array) ?
21
+ action.each {|a| _call_action(a, record)} :
22
+ _call_action(action, record)
23
+ end
24
+ end
25
+
26
+ def display_name
27
+ @display_name ||= name.to_s.gsub(/_/, ' ').capitalize
28
+ end
29
+
30
+ def for_select
31
+ [display_name, name.to_s]
32
+ end
33
+
34
+ def update(options = {})
35
+ if options.key?(:display) then
36
+ @display_name = options.delete(:display)
37
+ end
38
+ @options = options
39
+ self
40
+ end
41
+
42
+ private
43
+
44
+ def _call_action(action, record)
45
+ case action
46
+ when Symbol, String
47
+ record.send(action)
48
+ when Proc
49
+ action.call(record)
50
+ end
51
+ end
52
+
53
+ end