alexrevin-aasm_numerical 2.3.1

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