mikowitz-aasm 2.0.5

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.
data/lib/aasm.rb ADDED
@@ -0,0 +1,245 @@
1
+ require File.join(File.dirname(__FILE__), 'event')
2
+ require File.join(File.dirname(__FILE__), 'staged_event')
3
+ require File.join(File.dirname(__FILE__), 'state')
4
+ require File.join(File.dirname(__FILE__), 'state_machine')
5
+ require File.join(File.dirname(__FILE__), 'persistence')
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_default_role(set_role=nil)
56
+ if set_role
57
+ AASM::StateMachine[self].default_role = set_role
58
+ else
59
+ AASM::StateMachine[self].default_role
60
+ end
61
+ end
62
+
63
+ def aasm_default_role=(role)
64
+ AASM::StateMachine[self].default_role = role
65
+ end
66
+
67
+ def aasm_role(name)
68
+ sm = AASM::StateMachine[self]
69
+ sm.create_role(name)
70
+ sm.default_role = name unless sm.default_role
71
+ end
72
+
73
+ def aasm_event(name, options = {}, &block)
74
+ sm = AASM::StateMachine[self]
75
+
76
+ unless sm.events.has_key?(name)
77
+ sm.events[name] = AASM::SupportingClasses::Event.new(name, options, &block)
78
+ end
79
+
80
+ define_method("#{name.to_s}!") do |*args|
81
+ aasm_fire_event(name, true, *args)
82
+ end
83
+
84
+ define_method("#{name.to_s}") do |*args|
85
+ aasm_fire_event(name, false, *args)
86
+ end
87
+ end
88
+
89
+ def aasm_states
90
+ AASM::StateMachine[self].states
91
+ end
92
+
93
+ def aasm_roles
94
+ AASM::StateMachine[self].roles
95
+ end
96
+
97
+ def aasm_events
98
+ AASM::StateMachine[self].events
99
+ end
100
+
101
+ def aasm_states_for_select
102
+ AASM::StateMachine[self].states.map { |state| state.for_select }
103
+ end
104
+
105
+ end
106
+
107
+ # Instance methods
108
+ def aasm_current_state
109
+ return @aasm_current_state if @aasm_current_state
110
+
111
+ if self.respond_to?(:aasm_read_state) || self.private_methods.include?('aasm_read_state')
112
+ @aasm_current_state = aasm_read_state
113
+ end
114
+ return @aasm_current_state if @aasm_current_state
115
+ self.class.aasm_initial_state
116
+ end
117
+
118
+ def aasm_events_for_current_state
119
+ aasm_events_for_state(aasm_current_state)
120
+ end
121
+
122
+ def aasm_events_for_state(state)
123
+ events = self.class.aasm_events.values.select {|event| event.transitions_from_state?(state) }
124
+ events.map {|event| event.name}
125
+ end
126
+
127
+ private
128
+ def set_aasm_current_state_with_persistence(state)
129
+ save_success = true
130
+ if self.respond_to?(:aasm_write_state) || self.private_methods.include?('aasm_write_state')
131
+ save_success = aasm_write_state(state)
132
+ end
133
+ self.aasm_current_state = state if save_success
134
+
135
+ save_success
136
+ end
137
+
138
+ def aasm_current_state=(state)
139
+ if self.respond_to?(:aasm_write_state_without_persistence) || self.private_methods.include?('aasm_write_state_without_persistence')
140
+ aasm_write_state_without_persistence(state)
141
+ end
142
+ @aasm_current_state = state
143
+ end
144
+
145
+ def aasm_state_object_for_state(name)
146
+ obj = self.class.aasm_states.find {|s| s == name}
147
+ raise AASM::UndefinedState, "State :#{name} doesn't exist" if obj.nil?
148
+ obj
149
+ end
150
+
151
+ def aasm_fire_event(name, persist, *args)
152
+ args.unshift(nil) unless args.first.nil? or args.empty? or self.class.aasm_states.include?(args.first.to_sym)
153
+
154
+ aasm_state_object_for_state(aasm_current_state).call_action(:exit, self)
155
+
156
+ new_state = self.class.aasm_events[name].fire(self, *args)
157
+
158
+ unless new_state.nil?
159
+ aasm_state_object_for_state(new_state).call_action(:enter, self)
160
+
161
+ persist_successful = true
162
+ if persist
163
+ persist_successful = set_aasm_current_state_with_persistence(new_state)
164
+ self.class.aasm_events[name].execute_success_callback(self) if persist_successful
165
+ else
166
+ self.aasm_current_state = new_state
167
+ end
168
+
169
+ if persist_successful
170
+ self.aasm_event_fired(self.aasm_current_state, new_state) if self.respond_to?(:aasm_event_fired)
171
+ else
172
+ self.aasm_event_failed(name) if self.respond_to?(:aasm_event_failed)
173
+ end
174
+
175
+ persist_successful
176
+ else
177
+ if self.respond_to?(:aasm_event_failed)
178
+ self.aasm_event_failed(name)
179
+ end
180
+
181
+ false
182
+ end
183
+ end
184
+ end
185
+
186
+ module StagedAASM
187
+ def self.Version
188
+ '2.0.3'
189
+ end
190
+
191
+ class InvalidTransition < RuntimeError
192
+ end
193
+
194
+ def self.included(base) #:nodoc:
195
+ # TODO - need to ensure that a machine is being created because
196
+ # AASM was either included or arrived at via inheritance. It
197
+ # cannot be both.
198
+ base.extend StagedAASM::ClassMethods
199
+ end
200
+
201
+ module ClassMethods
202
+
203
+ def aasm_is_staged
204
+ AASM::StateMachine[self].config.is_staged = true
205
+ end
206
+
207
+ def aasm_admin_roles(*args)
208
+ sm = AASM::StateMachine[self]
209
+ args.each do |arg|
210
+ sm.admin_roles << arg if sm.roles.include?(arg)
211
+ end
212
+ end
213
+
214
+ def aasm_staging_roles(*args)
215
+ sm = AASM::StateMachine[self]
216
+ args.each do |arg|
217
+ sm.staging_roles << arg if sm.roles.include?(arg)
218
+ end
219
+ end
220
+
221
+ def admin_roles
222
+ AASM::StateMachine[self].admin_roles
223
+ end
224
+
225
+ def staging_roles
226
+ AASM::StateMachine[self].staging_roles
227
+ end
228
+
229
+ def aasm_staged_event(name, options = {}, &block)
230
+ sm = AASM::StateMachine[self]
231
+
232
+ unless sm.events.has_key?(name)
233
+ sm.events[name] = AASM::SupportingClasses::StagedEvent.new(name, options.merge({:admin_roles => sm.admin_roles, :staging_roles => sm.staging_roles}), &block)
234
+ end
235
+
236
+ define_method("#{name.to_s}!") do |*args|
237
+ aasm_fire_event(name, true, *args)
238
+ end
239
+
240
+ define_method("#{name.to_s}") do |*args|
241
+ aasm_fire_event(name, false, *args)
242
+ end
243
+ end
244
+ end
245
+ end
data/lib/event.rb ADDED
@@ -0,0 +1,61 @@
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
+ role = !AASM::StateMachine[obj.class].nil? ? (obj.class.aasm_roles & args.flatten).first || obj.class.aasm_default_role : nil
17
+
18
+ second_arg = obj.class.respond_to?(:aasm_roles) ? obj.class.aasm_roles : nil
19
+ transitions = @transitions.select { |t| t.from == obj.aasm_current_state and role_truth(t.for || second_arg, role) }
20
+ raise AASM::InvalidTransition, "Event '#{name}' cannot transition from '#{obj.aasm_current_state}' as #{role.to_s}" if transitions.size == 0
21
+
22
+ next_state = nil
23
+ transitions.each do |transition|
24
+ next if to_state and !Array(transition.to).include?(to_state)
25
+ if transition.perform(obj)
26
+ next_state = to_state || Array(transition.to).first
27
+ transition.execute(obj, *args)
28
+ break
29
+ end
30
+ end
31
+ next_state
32
+ end
33
+
34
+ def transitions_from_state?(state)
35
+ @transitions.any? { |t| t.from == state }
36
+ end
37
+
38
+ def execute_success_callback(obj)
39
+ case success
40
+ when String, Symbol:
41
+ obj.send(success)
42
+ when Array:
43
+ success.each { |meth| obj.send(meth) }
44
+ when Proc:
45
+ success.call(obj)
46
+ end
47
+ end
48
+
49
+ private
50
+ def transitions(trans_opts)
51
+ Array(trans_opts[:from]).each do |s|
52
+ @transitions << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
53
+ end
54
+ end
55
+
56
+ def role_truth(trans_for, role)
57
+ role.nil? ? true : trans_for.is_a?(Symbol) ? trans_for == role : trans_for.include?(role)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,246 @@
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
+ protected
111
+ def with_state_scope(state)
112
+ with_scope :find => {:conditions => ["#{table_name}.#{aasm_column} = ?", state.to_s]} do
113
+ yield if block_given?
114
+ end
115
+ end
116
+ end
117
+
118
+ module InstanceMethods
119
+
120
+ # Returns the current aasm_state of the object. Respects reload and
121
+ # any changes made to the aasm_state field directly
122
+ #
123
+ # Internally just calls <tt>aasm_read_state</tt>
124
+ #
125
+ # foo = Foo.find(1)
126
+ # foo.aasm_current_state # => :pending
127
+ # foo.aasm_state = "opened"
128
+ # foo.aasm_current_state # => :opened
129
+ # foo.close # => calls aasm_write_state_without_persistence
130
+ # foo.aasm_current_state # => :closed
131
+ # foo.reload
132
+ # foo.aasm_current_state # => :pending
133
+ #
134
+ def aasm_current_state
135
+ @current_state = aasm_read_state
136
+ end
137
+
138
+ private
139
+
140
+ # Ensures that if the aasm_state column is nil and the record is new
141
+ # that the initial state gets populated before validation on create
142
+ #
143
+ # foo = Foo.new
144
+ # foo.aasm_state # => nil
145
+ # foo.valid?
146
+ # foo.aasm_state # => "open" (where :open is the initial state)
147
+ #
148
+ #
149
+ # foo = Foo.find(:first)
150
+ # foo.aasm_state # => 1
151
+ # foo.aasm_state = nil
152
+ # foo.valid?
153
+ # foo.aasm_state # => nil
154
+ #
155
+ def aasm_ensure_initial_state
156
+ send("#{self.class.aasm_column}=", self.aasm_current_state.to_s)
157
+ end
158
+
159
+ end
160
+
161
+ module WriteStateWithoutPersistence
162
+ # Writes <tt>state</tt> to the state column, but does not persist it to the database
163
+ #
164
+ # foo = Foo.find(1)
165
+ # foo.aasm_current_state # => :opened
166
+ # foo.close
167
+ # foo.aasm_current_state # => :closed
168
+ # Foo.find(1).aasm_current_state # => :opened
169
+ # foo.save
170
+ # foo.aasm_current_state # => :closed
171
+ # Foo.find(1).aasm_current_state # => :closed
172
+ #
173
+ # NOTE: intended to be called from an event
174
+ def aasm_write_state_without_persistence(state)
175
+ write_attribute(self.class.aasm_column, state.to_s)
176
+ end
177
+ end
178
+
179
+ module WriteState
180
+ # Writes <tt>state</tt> to the state column and persists it to the database
181
+ # using update_attribute (which bypasses validation)
182
+ #
183
+ # foo = Foo.find(1)
184
+ # foo.aasm_current_state # => :opened
185
+ # foo.close!
186
+ # foo.aasm_current_state # => :closed
187
+ # Foo.find(1).aasm_current_state # => :closed
188
+ #
189
+ # NOTE: intended to be called from an event
190
+ def aasm_write_state(state)
191
+ old_value = read_attribute(self.class.aasm_column)
192
+ write_attribute(self.class.aasm_column, state.to_s)
193
+
194
+ unless self.save
195
+ write_attribute(self.class.aasm_column, old_value)
196
+ return false
197
+ end
198
+
199
+ true
200
+ end
201
+ end
202
+
203
+ module ReadState
204
+
205
+ # Returns the value of the aasm_column - called from <tt>aasm_current_state</tt>
206
+ #
207
+ # If it's a new record, and the aasm state column is blank it returns the initial state:
208
+ #
209
+ # class Foo < ActiveRecord::Base
210
+ # include AASM
211
+ # aasm_column :status
212
+ # aasm_state :opened
213
+ # aasm_state :closed
214
+ # end
215
+ #
216
+ # foo = Foo.new
217
+ # foo.current_state # => :opened
218
+ # foo.close
219
+ # foo.current_state # => :closed
220
+ #
221
+ # foo = Foo.find(1)
222
+ # foo.current_state # => :opened
223
+ # foo.aasm_state = nil
224
+ # foo.current_state # => nil
225
+ #
226
+ # NOTE: intended to be called from an event
227
+ #
228
+ # This allows for nil aasm states - be sure to add validation to your model
229
+ def aasm_read_state
230
+ if new_record?
231
+ send(self.class.aasm_column).blank? ? self.class.aasm_initial_state : send(self.class.aasm_column).to_sym
232
+ else
233
+ send(self.class.aasm_column).nil? ? nil : send(self.class.aasm_column).to_sym
234
+ end
235
+ end
236
+ end
237
+
238
+ module NamedScopeMethods
239
+ def aasm_state_with_named_scope name, options = {}
240
+ aasm_state_without_named_scope name, options
241
+ self.named_scope name, :conditions => {self.aasm_column => name.to_s} unless self.respond_to?(name)
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end