transitions 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # 1.0.6
2
+
3
+ * (troessner) Revert 'Fixing set_initial_state with Mongoid' because of https://github.com/troessner/transitions/issues/76
4
+ * (divins) Multiple success callbacks
5
+ * (cstrahan) Pass additional args to guard function
6
+ * (cmw) Support for configurable column names
7
+
1
8
  # 1.0.5
2
9
 
3
10
  * (troessner) Fix unhelpful error message when event can not be fired.
data/README.rdoc CHANGED
@@ -44,7 +44,7 @@ This goes into your Gemfile:
44
44
  end
45
45
  end
46
46
 
47
- In this example we assume that you are in a rails project using Bundler, which would automitcally require `transitions`.
47
+ In this example we assume that you are in a rails project using Bundler, which would automatically require `transitions`.
48
48
  If this is not the case for you you have to add
49
49
 
50
50
  require 'transitions'
@@ -98,6 +98,17 @@ you can use this feature a la:
98
98
  >> Order.pick_line_items
99
99
  => [#<Order id: 3, state: "pick_line_items", description: nil, created_at: "2011-08-23 15:48:46", updated_at: "2011-08-23 15:48:46">]
100
100
 
101
+ ==== Using <tt>guard</tt>
102
+
103
+ Each event definition takes an optional "guard" argument, which acts as a predicate for the transition.
104
+ You can pass in a Symbol, a String, or a Proc like this:
105
+
106
+ event :discontinue do
107
+ transitions :to => :discontinued, :from => [:available, :out_of_stock], :guard => :can_discontinue
108
+ end
109
+
110
+ Any arguments passed to the event method will be passed on to the <tt>guard</tt> predicate.
111
+
101
112
  ==== Using <tt>on_transition</tt>
102
113
 
103
114
  Each event definition takes an optional "on_transition" argument, which allows you to execute methods on transition.
@@ -107,6 +118,8 @@ You can pass in a Symbol, a String, a Proc or an Array containing method names a
107
118
  transitions :to => :discontinued, :from => [:available, :out_of_stock], :on_transition => [:do_discontinue, :notify_clerk]
108
119
  end
109
120
 
121
+ Any arguments passed to the event method will be passed on to the <tt>on_transition</tt> callback.
122
+
110
123
  ==== Using <tt>success</tt>
111
124
 
112
125
  In case you need to trigger a method call after a successful transition you can use <tt>success</tt>:
@@ -122,6 +135,12 @@ perfom some more complex success callbacks:
122
135
  transitions :to => :discontinued, :from => [:available, :out_of_stock]
123
136
  end
124
137
 
138
+ If you need it, you can even call multiple methods or lambdas just passing an array:
139
+
140
+ event :discontinue, :success => [:notify_admin, lambda { |order| AdminNotifier.notify_about_discontinued_order(order) }] do
141
+ transitions :to => :discontinued, :from => [:available, :out_of_stock]
142
+ end
143
+
125
144
  ==== Timestamps
126
145
 
127
146
  If you'd like to note the time of a state change, Transitions comes with timestamps free!
@@ -167,6 +186,20 @@ You can easily get a listing of all available states:
167
186
  state :closed
168
187
  end
169
188
 
189
+ === Configuring a different column name with ActiveRecord
190
+
191
+ To use a different column than <tt>state</tt> to track it's value simply do this:
192
+
193
+ class Product < ActiveRecord::Base
194
+ include Transitions
195
+
196
+ state_machine :attribute_name => :different_column do
197
+
198
+ ...
199
+
200
+ end
201
+ end
202
+
170
203
  === Documentation, Guides & Examples
171
204
 
172
205
  - {Online API Documentation}[http://rdoc.info/github/troessner/transitions/master/Transitions]
@@ -25,9 +25,31 @@ module ActiveModel
25
25
  extend ActiveSupport::Concern
26
26
 
27
27
  included do
28
+ class ::Transitions::Machine
29
+ unless instance_methods.include?(:new_initialize) || instance_methods.include?(:new_update)
30
+ attr_reader :attribute_name
31
+ alias :old_initialize :initialize
32
+ alias :old_update :update
33
+
34
+ def new_initialize(*args, &block)
35
+ @attribute_name = :state
36
+ old_initialize(*args, &block)
37
+ end
38
+
39
+ def new_update(options = {}, &block)
40
+ @attribute_name = options[:attribute_name] if options.key?(:attribute_name)
41
+ old_update(options, &block)
42
+ end
43
+
44
+ alias :initialize :new_initialize
45
+ alias :update :new_update
46
+ else
47
+ puts "WARNING: Transitions::Machine#new_update or Transitions::Machine#new_initialize already defined. This can possibly break ActiveModel::Transitions."
48
+ end
49
+ end
28
50
  include ::Transitions
29
51
  after_initialize :set_initial_state
30
- validates_presence_of :state
52
+ validate :state_presence
31
53
  validate :state_inclusion
32
54
  end
33
55
 
@@ -43,6 +65,10 @@ module ActiveModel
43
65
 
44
66
  protected
45
67
 
68
+ def transitions_state_column_name
69
+ self.class.state_machine.attribute_name
70
+ end
71
+
46
72
  def write_state(state)
47
73
  prev_state = current_state
48
74
  write_state_without_persistence(state)
@@ -55,22 +81,27 @@ module ActiveModel
55
81
  def write_state_without_persistence(state)
56
82
  ivar = self.class.get_state_machine.current_state_variable
57
83
  instance_variable_set(ivar, state)
58
- self.state = state.to_s
84
+ self[transitions_state_column_name] = state.to_s
59
85
  end
60
86
 
61
87
  def read_state
62
- self.state && self.state.to_sym
88
+ self[transitions_state_column_name] && self[transitions_state_column_name].to_sym
63
89
  end
64
90
 
65
91
  def set_initial_state
66
- self.state ||= self.class.get_state_machine.initial_state.to_s if self.respond_to?(:state=)
92
+ self[transitions_state_column_name] ||= self.class.get_state_machine.initial_state.to_s if self.has_attribute?(transitions_state_column_name)
93
+ end
94
+
95
+ def state_presence
96
+ unless self[transitions_state_column_name].present?
97
+ self.errors.add(transitions_state_column_name, :presence)
98
+ end
67
99
  end
68
100
 
69
101
  def state_inclusion
70
- unless self.class.get_state_machine.states.map{|s| s.name.to_s }.include?(self.state.to_s)
71
- self.errors.add(:state, :inclusion, :value => self.state)
102
+ unless self.class.get_state_machine.states.map{|s| s.name.to_s }.include?(self[transitions_state_column_name].to_s)
103
+ self.errors.add(transitions_state_column_name, :inclusion, :value => self[transitions_state_column_name])
72
104
  end
73
105
  end
74
106
  end
75
107
  end
76
-
@@ -49,7 +49,7 @@ module Transitions
49
49
  next_state = nil
50
50
  transitions.each do |transition|
51
51
  next if to_state && !Array(transition.to).include?(to_state)
52
- if transition.perform(obj)
52
+ if transition.perform(obj, *args)
53
53
  next_state = to_state || Array(transition.to).first
54
54
  transition.execute(obj, *args)
55
55
  break
@@ -78,7 +78,7 @@ module Transitions
78
78
  end
79
79
 
80
80
  def update(options = {}, &block)
81
- @success = build_sucess_callback(options[:success]) if options.key?(:success)
81
+ @success = build_success_callback(options[:success]) if options.key?(:success)
82
82
  self.timestamp = options[:timestamp] if options[:timestamp]
83
83
  instance_eval(&block) if block
84
84
  self
@@ -129,12 +129,18 @@ module Transitions
129
129
  end
130
130
  end
131
131
 
132
- def build_sucess_callback(callback_symbol_or_proc)
133
- case callback_symbol_or_proc
132
+ def build_success_callback(callback_names)
133
+ case callback_names
134
+ when Array
135
+ lambda do |record|
136
+ callback_names.each do |callback|
137
+ build_success_callback(callback).call(record)
138
+ end
139
+ end
134
140
  when Proc
135
- callback_symbol_or_proc
141
+ callback_names
136
142
  when Symbol
137
- lambda { |record| record.send(callback_symbol_or_proc) }
143
+ lambda { |record| record.send(callback_names) }
138
144
  end
139
145
  end
140
146
 
@@ -29,12 +29,12 @@ module Transitions
29
29
  @options = opts
30
30
  end
31
31
 
32
- def perform(obj)
32
+ def perform(obj, *args)
33
33
  case @guard
34
34
  when Symbol, String
35
- obj.send(@guard)
35
+ obj.send(@guard, *args)
36
36
  when Proc
37
- @guard.call(obj)
37
+ @guard.call(obj, *args)
38
38
  else
39
39
  true
40
40
  end
@@ -1,3 +1,3 @@
1
1
  module Transitions
2
- VERSION = '0.1.5'
2
+ VERSION = '0.1.6'
3
3
  end
@@ -11,6 +11,17 @@ end
11
11
 
12
12
  set_up_db CreateTrafficLights
13
13
 
14
+ class CreateDifferentTrafficLights < ActiveRecord::Migration
15
+ def self.up
16
+ create_table(:different_traffic_lights) do |t|
17
+ t.string :different_state
18
+ t.string :name
19
+ end
20
+ end
21
+ end
22
+
23
+ set_up_db CreateDifferentTrafficLights
24
+
14
25
  class TrafficLight < ActiveRecord::Base
15
26
  include ActiveModel::Transitions
16
27
 
@@ -147,3 +158,148 @@ class TestActiveRecord < Test::Unit::TestCase
147
158
  assert_equal "red", @light.reload.state
148
159
  end
149
160
  end
161
+
162
+ class TestNewActiveRecord < TestActiveRecord
163
+
164
+ def setup
165
+ set_up_db CreateTrafficLights
166
+ @light = TrafficLight.new
167
+ end
168
+
169
+ test "new active records defaults current state to the initial state" do
170
+ assert_equal :off, @light.current_state
171
+ end
172
+
173
+ end
174
+
175
+ class TestScopes < Test::Unit::TestCase
176
+ test "scope returns correct object" do
177
+ @light = TrafficLight.create!
178
+ assert_respond_to TrafficLight, :off
179
+ assert_equal TrafficLight.off.first, @light
180
+ assert TrafficLight.red.empty?
181
+ end
182
+
183
+ test "scopes exist" do
184
+ assert_respond_to TrafficLight, :off
185
+ assert_respond_to TrafficLight, :red
186
+ assert_respond_to TrafficLight, :green
187
+ assert_respond_to TrafficLight, :yellow
188
+ end
189
+
190
+ test 'scopes are only generated if we explicitly say so' do
191
+ assert_not_respond_to LightBulb, :off
192
+ assert_not_respond_to LightBulb, :on
193
+ end
194
+
195
+ test 'scope generation raises an exception if we try to overwrite an existing method' do
196
+ assert_raise(Transitions::InvalidMethodOverride) {
197
+ class Light < ActiveRecord::Base
198
+ include ActiveModel::Transitions
199
+
200
+ state_machine :auto_scopes => true do
201
+ state :new
202
+ state :broken
203
+ end
204
+ end
205
+ }
206
+ end
207
+ end
208
+
209
+ class DifferentTrafficLight < ActiveRecord::Base
210
+ include ActiveModel::Transitions
211
+
212
+ state_machine :attribute_name => :different_state, :auto_scopes => true do
213
+ state :off
214
+
215
+ state :red
216
+ state :green
217
+ state :yellow
218
+
219
+ event :red_on do
220
+ transitions :to => :red, :from => [:yellow]
221
+ end
222
+
223
+ event :green_on do
224
+ transitions :to => :green, :from => [:red]
225
+ end
226
+
227
+ event :yellow_on do
228
+ transitions :to => :yellow, :from => [:green]
229
+ end
230
+
231
+ event :reset do
232
+ transitions :to => :red, :from => [:off]
233
+ end
234
+ end
235
+ end
236
+
237
+ class TestActiveRecordWithDifferentColumnName < Test::Unit::TestCase
238
+ def setup
239
+ set_up_db CreateDifferentTrafficLights
240
+ @light = DifferentTrafficLight.create!
241
+ end
242
+
243
+ test "new record has the initial state set" do
244
+ @light = DifferentTrafficLight.new
245
+ assert_equal "off", @light.different_state
246
+ end
247
+
248
+ test "states initial state" do
249
+ assert @light.off?
250
+ assert_equal :off, @light.current_state
251
+ end
252
+
253
+ test "transition to a valid state" do
254
+ @light.reset
255
+ assert @light.red?
256
+ assert_equal :red, @light.current_state
257
+
258
+ @light.green_on
259
+ assert @light.green?
260
+ assert_equal :green, @light.current_state
261
+ end
262
+
263
+ test "transition does not persist state" do
264
+ @light.reset
265
+ assert_equal :red, @light.current_state
266
+ @light.reload
267
+ assert_equal "off", @light.different_state
268
+ end
269
+
270
+ test "transition does persists state" do
271
+ @light.reset!
272
+ assert_equal :red, @light.current_state
273
+ @light.reload
274
+ assert_equal "red", @light.different_state
275
+ end
276
+
277
+ test "transition to an invalid state" do
278
+ assert_raise(Transitions::InvalidTransition) { @light.yellow_on }
279
+ assert_equal :off, @light.current_state
280
+ end
281
+
282
+ test "transition with wrong state will not validate" do
283
+ for s in @light.class.state_machine.states
284
+ @light.different_state = s.name
285
+ assert @light.valid?
286
+ end
287
+ @light.different_state = "invalid_one"
288
+ assert_false @light.valid?
289
+ end
290
+
291
+ test "reloading model resets current state" do
292
+ @light.reset
293
+ assert @light.red?
294
+ @light.update_attribute(:different_state, 'green')
295
+ assert @light.reload.green?, "reloaded state should come from database, not instance variable"
296
+ end
297
+
298
+ test "calling non-bang event updates state attribute" do
299
+ @light.reset!
300
+ assert @light.red?
301
+ @light.green_on
302
+ assert_equal "green", @light.different_state
303
+ assert_equal "red", @light.reload.different_state
304
+ end
305
+ end
@@ -5,6 +5,7 @@ class TestEvent < Test::Unit::TestCase
5
5
  @state_name = :close_order
6
6
  @success_as_symbol = :success_callback
7
7
  @success_as_lambda = lambda { |record| record.success_callback }
8
+ @success_as_array = [@success_as_symbol, @success_as_lambda]
8
9
  end
9
10
 
10
11
  def event_with_symbol_success_callback
@@ -20,6 +21,12 @@ class TestEvent < Test::Unit::TestCase
20
21
  end
21
22
  end
22
23
 
24
+ def event_with_array_success_callback
25
+ @event = Transitions::Event.new(nil, @state_name, {:success => @success_as_array}) do
26
+ transitions :to => :closed, :from => [:open, :received]
27
+ end
28
+ end
29
+
23
30
  test "should set the name" do
24
31
  assert_equal @state_name, new_event.name
25
32
  end
@@ -39,6 +46,24 @@ class TestEvent < Test::Unit::TestCase
39
46
  assert_respond_to event_with_lambda_success_callback.success, :call
40
47
  end
41
48
 
49
+ test "should build a block which calls the given success_callback lambda on the passed record instance" do
50
+ record = mock("SomeRecordToGetCalled")
51
+ record.expects(:success_callback)
52
+
53
+ event_with_lambda_success_callback.success.call(record)
54
+ end
55
+
56
+ test "should set the success callback with an array" do
57
+ assert_respond_to event_with_array_success_callback.success, :call
58
+ end
59
+
60
+ test "should build a block which calls the given success_callback array on the passed record instance for each callback" do
61
+ record = mock("SomeRecordToGetCalled")
62
+ record.expects(:success_callback).twice
63
+
64
+ event_with_array_success_callback.success.call(record)
65
+ end
66
+
42
67
  test "should create StateTransitions" do
43
68
  Transitions::StateTransition.expects(:new).with(:to => :closed, :from => :open)
44
69
  Transitions::StateTransition.expects(:new).with(:to => :closed, :from => :received)
@@ -1,11 +1,13 @@
1
1
  require "helper"
2
2
 
3
3
  class TestStateTransitionGuardCheck < Test::Unit::TestCase
4
+ args = [:foo, "bar"]
5
+
4
6
  test "should return true of there is no guard" do
5
7
  opts = {:from => "foo", :to => "bar"}
6
8
  st = Transitions::StateTransition.new(opts)
7
9
 
8
- assert st.perform(nil)
10
+ assert st.perform(nil, *args)
9
11
  end
10
12
 
11
13
  test "should call the method on the object if guard is a symbol" do
@@ -13,9 +15,9 @@ class TestStateTransitionGuardCheck < Test::Unit::TestCase
13
15
  st = Transitions::StateTransition.new(opts)
14
16
 
15
17
  obj = stub
16
- obj.expects(:test_guard)
18
+ obj.expects(:test_guard).with(*args)
17
19
 
18
- st.perform(obj)
20
+ st.perform(obj, *args)
19
21
  end
20
22
 
21
23
  test "should call the method on the object if guard is a string" do
@@ -23,18 +25,18 @@ class TestStateTransitionGuardCheck < Test::Unit::TestCase
23
25
  st = Transitions::StateTransition.new(opts)
24
26
 
25
27
  obj = stub
26
- obj.expects(:test_guard)
28
+ obj.expects(:test_guard).with(*args)
27
29
 
28
- st.perform(obj)
30
+ st.perform(obj, *args)
29
31
  end
30
32
 
31
33
  test "should call the proc passing the object if the guard is a proc" do
32
- opts = {:from => "foo", :to => "bar", :guard => Proc.new {|o| o.test_guard}}
34
+ opts = {:from => "foo", :to => "bar", :guard => Proc.new {|o, *args| o.test_guard(*args)}}
33
35
  st = Transitions::StateTransition.new(opts)
34
36
 
35
37
  obj = stub
36
- obj.expects(:test_guard)
38
+ obj.expects(:test_guard).with(*args)
37
39
 
38
- st.perform(obj)
40
+ st.perform(obj, *args)
39
41
  end
40
42
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: transitions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-01-28 00:00:00.000000000 Z
13
+ date: 2013-02-28 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: bundler
@@ -179,7 +179,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
179
179
  version: '0'
180
180
  segments:
181
181
  - 0
182
- hash: -1071042129
182
+ hash: -1024202641
183
183
  required_rubygems_version: !ruby/object:Gem::Requirement
184
184
  none: false
185
185
  requirements: