prepor-aasm 2.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,160 @@
1
+ require File.join(File.dirname(__FILE__), 'event')
2
+ require File.join(File.dirname(__FILE__), 'state')
3
+ require File.join(File.dirname(__FILE__), 'state_machine')
4
+ require File.join(File.dirname(__FILE__), 'persistence')
5
+ require File.join(File.dirname(__FILE__), 'integers')
6
+
7
+ module AASM
8
+ def self.Version
9
+ '2.0.5'
10
+ end
11
+
12
+ class InvalidTransition < RuntimeError
13
+ end
14
+
15
+ class UndefinedState < RuntimeError
16
+ end
17
+
18
+ def self.included(base) #:nodoc:
19
+ # TODO - need to ensure that a machine is being created because
20
+ # AASM was either included or arrived at via inheritance. It
21
+ # cannot be both.
22
+ base.extend AASM::ClassMethods
23
+ AASM::Persistence.set_persistence(base)
24
+ AASM::StateMachine[base] = AASM::StateMachine.new('')
25
+ end
26
+
27
+ module ClassMethods
28
+ def inherited(klass)
29
+ AASM::StateMachine[klass] = AASM::StateMachine[self].clone
30
+ super
31
+ end
32
+
33
+ def aasm_initial_state(set_state=nil)
34
+ if set_state
35
+ AASM::StateMachine[self].initial_state = set_state
36
+ else
37
+ AASM::StateMachine[self].initial_state
38
+ end
39
+ end
40
+
41
+ def aasm_initial_state=(state)
42
+ AASM::StateMachine[self].initial_state = state
43
+ end
44
+
45
+ def aasm_state(name, options={})
46
+ sm = AASM::StateMachine[self]
47
+ sm.create_state(name, options)
48
+ sm.initial_state = name unless sm.initial_state
49
+
50
+ define_method("#{name.to_s}?") do
51
+ aasm_current_state == name
52
+ end
53
+ end
54
+
55
+ def aasm_event(name, options = {}, &block)
56
+ sm = AASM::StateMachine[self]
57
+
58
+ unless sm.events.has_key?(name)
59
+ sm.events[name] = AASM::SupportingClasses::Event.new(name, options, &block)
60
+ end
61
+
62
+ define_method("#{name.to_s}!") do |*args|
63
+ aasm_fire_event(name, true, *args)
64
+ end
65
+
66
+ define_method("#{name.to_s}") do |*args|
67
+ aasm_fire_event(name, false, *args)
68
+ end
69
+ end
70
+
71
+ def aasm_states
72
+ AASM::StateMachine[self].states
73
+ end
74
+
75
+ def aasm_events
76
+ AASM::StateMachine[self].events
77
+ end
78
+
79
+ def aasm_states_for_select
80
+ AASM::StateMachine[self].states.map { |state| state.for_select }
81
+ end
82
+
83
+ end
84
+
85
+ # Instance methods
86
+ def aasm_current_state
87
+ return @aasm_current_state if @aasm_current_state
88
+
89
+ if self.respond_to?(:aasm_read_state) || self.private_methods.include?('aasm_read_state')
90
+ @aasm_current_state = aasm_read_state
91
+ end
92
+ return @aasm_current_state if @aasm_current_state
93
+ self.class.aasm_initial_state
94
+ end
95
+
96
+ def aasm_events_for_current_state
97
+ aasm_events_for_state(aasm_current_state)
98
+ end
99
+
100
+ def aasm_events_for_state(state)
101
+ events = self.class.aasm_events.values.select {|event| event.transitions_from_state?(state) }
102
+ events.map {|event| event.name}
103
+ end
104
+
105
+ private
106
+ def set_aasm_current_state_with_persistence(state)
107
+ save_success = true
108
+ if self.respond_to?(:aasm_write_state) || self.private_methods.include?('aasm_write_state')
109
+ save_success = aasm_write_state(state)
110
+ end
111
+ self.aasm_current_state = state if save_success
112
+
113
+ save_success
114
+ end
115
+
116
+ def aasm_current_state=(state)
117
+ if self.respond_to?(:aasm_write_state_without_persistence) || self.private_methods.include?('aasm_write_state_without_persistence')
118
+ aasm_write_state_without_persistence(state)
119
+ end
120
+ @aasm_current_state = state
121
+ end
122
+
123
+ def aasm_state_object_for_state(name)
124
+ obj = self.class.aasm_states.find {|s| s == name}
125
+ raise AASM::UndefinedState, "State :#{name} doesn't exist" if obj.nil?
126
+ obj
127
+ end
128
+
129
+ def aasm_fire_event(name, persist, *args)
130
+ aasm_state_object_for_state(aasm_current_state).call_action(:exit, self)
131
+
132
+ new_state = self.class.aasm_events[name].fire(self, *args)
133
+
134
+ unless new_state.nil?
135
+ aasm_state_object_for_state(new_state).call_action(:enter, self)
136
+
137
+ persist_successful = true
138
+ if persist
139
+ persist_successful = set_aasm_current_state_with_persistence(new_state)
140
+ self.class.aasm_events[name].execute_success_callback(self) if persist_successful
141
+ else
142
+ self.aasm_current_state = new_state
143
+ end
144
+
145
+ if persist_successful
146
+ self.aasm_event_fired(self.aasm_current_state, new_state) if self.respond_to?(:aasm_event_fired)
147
+ else
148
+ self.aasm_event_failed(name) if self.respond_to?(:aasm_event_failed)
149
+ end
150
+
151
+ persist_successful
152
+ else
153
+ if self.respond_to?(:aasm_event_failed)
154
+ self.aasm_event_failed(name)
155
+ end
156
+
157
+ false
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,53 @@
1
+ require File.join(File.dirname(__FILE__), 'state_transition')
2
+
3
+ module AASM
4
+ module SupportingClasses
5
+ class Event
6
+ attr_reader :name, :success
7
+
8
+ def initialize(name, options = {}, &block)
9
+ @name = name
10
+ @success = options[:success]
11
+ @transitions = []
12
+ instance_eval(&block) if block
13
+ end
14
+
15
+ def fire(obj, to_state=nil, *args)
16
+ transitions = @transitions.select { |t| t.from == obj.aasm_current_state }
17
+ raise AASM::InvalidTransition, "Event '#{name}' cannot transition from '#{obj.aasm_current_state}'" if transitions.size == 0
18
+
19
+ next_state = nil
20
+ transitions.each do |transition|
21
+ next if to_state and !Array(transition.to).include?(to_state)
22
+ if transition.perform(obj) && transition.execute(obj, *args)
23
+ next_state = to_state || Array(transition.to).first
24
+ break
25
+ end
26
+ end
27
+ next_state
28
+ end
29
+
30
+ def transitions_from_state?(state)
31
+ @transitions.any? { |t| t.from == state }
32
+ end
33
+
34
+ def execute_success_callback(obj)
35
+ case success
36
+ when String, Symbol
37
+ obj.send(success)
38
+ when Array
39
+ success.each { |meth| obj.send(meth) }
40
+ when Proc
41
+ success.call(obj)
42
+ end
43
+ end
44
+
45
+ private
46
+ def transitions(trans_opts)
47
+ Array(trans_opts[:from]).each do |s|
48
+ @transitions << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ module AASM
2
+ module SupportingClasses
3
+ class Integers
4
+ attr_accessor :hash
5
+ def initialize
6
+ self.hash = {}
7
+ end
8
+
9
+ def add_integer(state)
10
+ self.hash[state.options[:integer]] = state.name
11
+ end
12
+
13
+ def [](name)
14
+ by_state(name)
15
+ end
16
+
17
+ def by_state(name)
18
+ self.hash.invert[name]
19
+ end
20
+
21
+ def by_integer(i)
22
+ self.hash[i]
23
+ end
24
+
25
+ def setted?
26
+ !self.hash.nil?
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ module AASM
2
+ module Persistence
3
+
4
+ # Checks to see this class or any of it's superclasses inherit from
5
+ # ActiveRecord::Base and if so includes ActiveRecordPersistence
6
+ def self.set_persistence(base)
7
+ # Use a fancier auto-loading thingy, perhaps. When there are more persistence engines.
8
+ hierarchy = base.ancestors.map {|klass| klass.to_s}
9
+
10
+ if hierarchy.include?("ActiveRecord::Base")
11
+ require File.join(File.dirname(__FILE__), 'persistence', 'active_record_persistence')
12
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,266 @@
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_on_create :aasm_ensure_initial_state
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)
42
+ base.extend(AASM::Persistence::ActiveRecordPersistence::NamedScopeMethods)
43
+
44
+ base.class_eval do
45
+ class << self
46
+ alias_method :aasm_state_without_named_scope, :aasm_state
47
+ alias_method :aasm_state, :aasm_state_with_named_scope
48
+ end
49
+ end
50
+ end
51
+
52
+ base.before_validation_on_create :aasm_ensure_initial_state
53
+ end
54
+
55
+ module ClassMethods
56
+ # Maps to the aasm_column in the database. Deafults to "aasm_state". You can write:
57
+ #
58
+ # create_table :foos do |t|
59
+ # t.string :name
60
+ # t.string :aasm_state
61
+ # end
62
+ #
63
+ # class Foo < ActiveRecord::Base
64
+ # include AASM
65
+ # end
66
+ #
67
+ # OR:
68
+ #
69
+ # create_table :foos do |t|
70
+ # t.string :name
71
+ # t.string :status
72
+ # end
73
+ #
74
+ # class Foo < ActiveRecord::Base
75
+ # include AASM
76
+ # aasm_column :status
77
+ # end
78
+ #
79
+ # This method is both a getter and a setter
80
+ def aasm_column(column_name=nil)
81
+ if column_name
82
+ AASM::StateMachine[self].config.column = column_name.to_sym
83
+ # @aasm_column = column_name.to_sym
84
+ else
85
+ AASM::StateMachine[self].config.column ||= :aasm_state
86
+ # @aasm_column ||= :aasm_state
87
+ end
88
+ # @aasm_column
89
+ AASM::StateMachine[self].config.column
90
+ end
91
+
92
+ def find_in_state(number, state, *args)
93
+ with_state_scope state do
94
+ find(number, *args)
95
+ end
96
+ end
97
+
98
+ def count_in_state(state, *args)
99
+ with_state_scope state do
100
+ count(*args)
101
+ end
102
+ end
103
+
104
+ def calculate_in_state(state, *args)
105
+ with_state_scope state do
106
+ calculate(*args)
107
+ end
108
+ end
109
+
110
+ def aasm_integers
111
+ AASM::StateMachine[self].integers
112
+ end
113
+
114
+ # Get state name by integer if integers are set
115
+ def aasm_state_name(value)
116
+ aasm_integers.setted? && value.is_a?(Integer) ? aasm_integers.by_integer(value) : value.to_s
117
+ end
118
+
119
+ # Get state integer by symbol if integers are set
120
+ def aasm_state_integer(value)
121
+ aasm_integers.setted? && value.is_a?(Symbol) ? aasm_integers.by_state(value) : value
122
+ end
123
+
124
+
125
+ protected
126
+ def with_state_scope(state)
127
+ with_scope :find => {:conditions => ["#{table_name}.#{aasm_column} = ?", state.to_s]} do
128
+ yield if block_given?
129
+ end
130
+ end
131
+ end
132
+
133
+ module InstanceMethods
134
+
135
+ # Returns the current aasm_state of the object. Respects reload and
136
+ # any changes made to the aasm_state field directly
137
+ #
138
+ # Internally just calls <tt>aasm_read_state</tt>
139
+ #
140
+ # foo = Foo.find(1)
141
+ # foo.aasm_current_state # => :pending
142
+ # foo.aasm_state = "opened"
143
+ # foo.aasm_current_state # => :opened
144
+ # foo.close # => calls aasm_write_state_without_persistence
145
+ # foo.aasm_current_state # => :closed
146
+ # foo.reload
147
+ # foo.aasm_current_state # => :pending
148
+ #
149
+ def aasm_current_state
150
+ @current_state = aasm_read_state
151
+ end
152
+
153
+ def aasm_column_value
154
+ send(self.class.aasm_column)
155
+ end
156
+
157
+
158
+ private
159
+
160
+ # Ensures that if the aasm_state column is nil and the record is new
161
+ # that the initial state gets populated before validation on create
162
+ #
163
+ # foo = Foo.new
164
+ # foo.aasm_state # => nil
165
+ # foo.valid?
166
+ # foo.aasm_state # => "open" (where :open is the initial state)
167
+ #
168
+ #
169
+ # foo = Foo.find(:first)
170
+ # foo.aasm_state # => 1
171
+ # foo.aasm_state = nil
172
+ # foo.valid?
173
+ # foo.aasm_state # => nil
174
+ #
175
+ def aasm_ensure_initial_state
176
+ send("#{self.class.aasm_column}=", self.aasm_current_state.to_s)
177
+ end
178
+
179
+ end
180
+
181
+ module WriteStateWithoutPersistence
182
+ # Writes <tt>state</tt> to the state column, but does not persist it to the database
183
+ #
184
+ # foo = Foo.find(1)
185
+ # foo.aasm_current_state # => :opened
186
+ # foo.close
187
+ # foo.aasm_current_state # => :closed
188
+ # Foo.find(1).aasm_current_state # => :opened
189
+ # foo.save
190
+ # foo.aasm_current_state # => :closed
191
+ # Foo.find(1).aasm_current_state # => :closed
192
+ #
193
+ # NOTE: intended to be called from an event
194
+ def aasm_write_state_without_persistence(state)
195
+ write_attribute(self.class.aasm_column, self.class.aasm_state_integer(state))
196
+ end
197
+ end
198
+
199
+ module WriteState
200
+ # Writes <tt>state</tt> to the state column and persists it to the database
201
+ # using update_attribute (which bypasses validation)
202
+ #
203
+ # foo = Foo.find(1)
204
+ # foo.aasm_current_state # => :opened
205
+ # foo.close!
206
+ # foo.aasm_current_state # => :closed
207
+ # Foo.find(1).aasm_current_state # => :closed
208
+ #
209
+ # NOTE: intended to be called from an event
210
+ def aasm_write_state(state)
211
+ old_value = read_attribute(self.class.aasm_column)
212
+ write_attribute(self.class.aasm_column, self.class.aasm_state_integer(state))
213
+
214
+ unless self.save
215
+ write_attribute(self.class.aasm_column, old_value)
216
+ return false
217
+ end
218
+
219
+ true
220
+ end
221
+ end
222
+
223
+ module ReadState
224
+
225
+ # Returns the value of the aasm_column - called from <tt>aasm_current_state</tt>
226
+ #
227
+ # If it's a new record, and the aasm state column is blank it returns the initial state:
228
+ #
229
+ # class Foo < ActiveRecord::Base
230
+ # include AASM
231
+ # aasm_column :status
232
+ # aasm_state :opened
233
+ # aasm_state :closed
234
+ # end
235
+ #
236
+ # foo = Foo.new
237
+ # foo.current_state # => :opened
238
+ # foo.close
239
+ # foo.current_state # => :closed
240
+ #
241
+ # foo = Foo.find(1)
242
+ # foo.current_state # => :opened
243
+ # foo.aasm_state = nil
244
+ # foo.current_state # => nil
245
+ #
246
+ # NOTE: intended to be called from an event
247
+ #
248
+ # This allows for nil aasm states - be sure to add validation to your model
249
+ def aasm_read_state
250
+ state = if new_record?
251
+ aasm_column_value.blank? ? self.class.aasm_initial_state : self.class.aasm_state_name(aasm_column_value).to_sym
252
+ else
253
+ aasm_column_value.nil? ? nil : self.class.aasm_state_name(aasm_column_value).to_sym
254
+ end
255
+ end
256
+ end
257
+
258
+ module NamedScopeMethods
259
+ def aasm_state_with_named_scope name, options = {}
260
+ aasm_state_without_named_scope name, options
261
+ self.named_scope name, lambda { {:conditions => {self.aasm_column => self.aasm_state_integer(name)} } } unless self.respond_to?(name)
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end