transitions 0.0.10 → 0.0.11

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/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  pkg
2
2
  *.gem
3
3
  .bundle
4
+ .rvmrc
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- transitions (0.0.10)
4
+ transitions (0.0.11)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Jakub Kuźma
1
+ Copyright (c) 2010 Jakub Kuźma, Timo Rößner
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -30,6 +30,29 @@ I really encourage you to try {state_machine}[https://github.com/pluginaweek/sta
30
30
  end
31
31
  end
32
32
 
33
+ == Automatic scope generation
34
+
35
+ `transitions` will automatically generate scopes for you if you are using AR:
36
+
37
+ Given a model like this:
38
+
39
+ class Order < ActiveRecord::Base
40
+ include ActiveRecord::Transitions
41
+ state_machine do
42
+ state :pick_line_items
43
+ state :picking_line_items
44
+ end
45
+ end
46
+
47
+ you can use this feature a la:
48
+
49
+ >> Order.pick_line_items
50
+ => []
51
+ >> Order.create!
52
+ => #<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">
53
+ >> Order.pick_line_items
54
+ => [#<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">]
55
+
33
56
  == Using on_transition
34
57
 
35
58
  Each event definition takes an optional "on_transition" argument, which allows you to execute methods on transition.
@@ -39,6 +62,25 @@ You can pass in a Symbol, a String, a Proc or an Array containing method names a
39
62
  transitions :to => :discontinued, :from => [:available, :out_of_stock], :on_transition => [:do_discontinue, :notify_clerk]
40
63
  end
41
64
 
65
+ == Timestamps
66
+
67
+ If you'd like to note the time of a state change, Transitions comes with timestamps free!
68
+ To activate them, simply pass the :timestamp option to the event definition with a value of either true or
69
+ the name of the timestamp column.
70
+ *NOTE - This should be either true, a String or a Symbol*
71
+
72
+ # This will look for an attribute called exploded_at or exploded_on (in that order)
73
+ # If present, it will be updated
74
+ event :explode, :timestamp => true do
75
+ transitions :from => :complete, :to => :exploded
76
+ end
77
+
78
+ # This will look for an attribute named repaired_on to update upon save
79
+ event :rebuild, :timestamp => :repaired_on do
80
+ transiions :from => :exploded, :to => :rebuilt
81
+ end
82
+
83
+
42
84
  == Using with Rails
43
85
 
44
86
  This goes into your Gemfile:
@@ -86,4 +128,4 @@ bang(!)-version will call <tt>write_state</tt>.
86
128
 
87
129
  == Copyright
88
130
 
89
- Copyright (c) 2010 Jakub Kuźma. See LICENSE for details.
131
+ Copyright (c) 2010 Jakub Kuźma, Timo Rößner. See LICENSE for details.
@@ -30,8 +30,11 @@ module ActiveRecord
30
30
  validates_presence_of :state
31
31
  validate :state_inclusion
32
32
  end
33
-
34
- def reload
33
+
34
+ # The optional options argument is passed to find when reloading so you may
35
+ # do e.g. record.reload(:lock => true) to reload the same record with an
36
+ # exclusive row lock.
37
+ def reload(options = nil)
35
38
  super.tap do
36
39
  self.class.state_machines.values.each do |sm|
37
40
  remove_instance_variable(sm.current_state_variable) if instance_variable_defined?(sm.current_state_variable)
data/lib/transitions.rb CHANGED
@@ -58,7 +58,7 @@ module Transitions
58
58
  def define_state_query_method(state_name)
59
59
  name = "#{state_name}?"
60
60
  undef_method(name) if method_defined?(name)
61
- class_eval "def #{name}; current_state.to_s == %(#{state_name}) end"
61
+ define_method(name) { current_state.to_s == %(#{state_name}) }
62
62
  end
63
63
  end
64
64
 
@@ -22,7 +22,7 @@
22
22
 
23
23
  module Transitions
24
24
  class Event
25
- attr_reader :name, :success
25
+ attr_reader :name, :success, :timestamp
26
26
 
27
27
  def initialize(machine, name, options = {}, &block)
28
28
  @machine, @name, @transitions = machine, name, []
@@ -51,6 +51,8 @@ module Transitions
51
51
  break
52
52
  end
53
53
  end
54
+ # Update timestamps on obj if a timestamp has been defined
55
+ update_event_timestamp(obj, next_state) if timestamp_defined?
54
56
  next_state
55
57
  end
56
58
 
@@ -65,14 +67,58 @@ module Transitions
65
67
  name == event.name
66
68
  end
67
69
  end
70
+
71
+ # Has the timestamp option been specified for this event?
72
+ def timestamp_defined?
73
+ !@timestamp.nil?
74
+ end
68
75
 
69
76
  def update(options = {}, &block)
70
- @success = options[:success] if options.key?(:success)
77
+ @success = options[:success] if options.key?(:success)
78
+ self.timestamp = options[:timestamp] if options[:timestamp]
71
79
  instance_eval(&block) if block
72
80
  self
73
81
  end
82
+
83
+ # update the timestamp attribute on obj
84
+ def update_event_timestamp(obj, next_state)
85
+ obj.send "#{timestamp_attribute_name(obj, next_state)}=", Time.now
86
+ end
87
+
88
+ # Set the timestamp attribute.
89
+ # @raise [ArgumentError] timestamp should be either a String, Symbol or true
90
+ def timestamp=(value)
91
+ case value
92
+ when String, Symbol, TrueClass
93
+ @timestamp = value
94
+ else
95
+ raise ArgumentError, "timestamp must be either: true, a String or a Symbol"
96
+ end
97
+ end
98
+
74
99
 
75
100
  private
101
+
102
+ # Returns the name of the timestamp attribute for this event
103
+ # If the timestamp was simply true it returns the default_timestamp_name
104
+ # otherwise, returns the user-specified timestamp name
105
+ def timestamp_attribute_name(obj, next_state)
106
+ timestamp == true ? default_timestamp_name(obj, next_state) : @timestamp
107
+ end
108
+
109
+ # If @timestamp is true, try a default timestamp name
110
+ def default_timestamp_name(obj, next_state)
111
+ at_name = "%s_at" % next_state
112
+ on_name = "%s_on" % next_state
113
+ case
114
+ when obj.respond_to?(at_name) then at_name
115
+ when obj.respond_to?(on_name) then on_name
116
+ else
117
+ raise NoMethodError, "Couldn't find a suitable timestamp field for event: #{@name}.
118
+ Please define #{at_name} or #{on_name} in #{obj.class}"
119
+ end
120
+ end
121
+
76
122
 
77
123
  def transitions(trans_opts)
78
124
  Array(trans_opts[:from]).each do |s|
@@ -38,6 +38,7 @@ module Transitions
38
38
  def update(options = {}, &block)
39
39
  @initial_state = options[:initial] if options.key?(:initial)
40
40
  instance_eval(&block) if block
41
+ include_scopes if defined?(ActiveRecord::Base) && @klass < ActiveRecord::Base
41
42
  self
42
43
  end
43
44
 
@@ -92,6 +93,12 @@ module Transitions
92
93
  def event_failed_callback
93
94
  @event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed'
94
95
  end
96
+
97
+ def include_scopes
98
+ @states.each do |state|
99
+ @klass.scope state.name.to_sym, @klass.where(:state => state.name.to_s)
100
+ end
101
+ end
95
102
  end
96
103
  end
97
104
 
@@ -1,3 +1,3 @@
1
1
  module Transitions
2
- VERSION = "0.0.10"
2
+ VERSION = "0.0.11"
3
3
  end
@@ -0,0 +1,19 @@
1
+ # Use this schema to create all required tables
2
+ class CreateDb < ActiveRecord::Migration
3
+ def self.up
4
+ create_table(:traffic_lights, force: true) do |t|
5
+ t.string :state
6
+ t.string :name
7
+ end
8
+
9
+ create_table(:orders, force: true) do |t|
10
+ t.string :state
11
+ t.string :order_number
12
+ t.datetime :paid_at
13
+ t.datetime :prepared_on
14
+ t.datetime :dispatched_at
15
+ t.date :cancellation_date
16
+ end
17
+
18
+ end
19
+ end
data/test/helper.rb CHANGED
@@ -3,6 +3,7 @@ require "test/unit"
3
3
  require "active_support/all"
4
4
  require "active_record"
5
5
  require "mocha"
6
+ require "db/create_db"
6
7
 
7
8
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
8
9
  $LOAD_PATH.unshift(File.dirname(__FILE__))
@@ -12,3 +13,9 @@ require "active_record/transitions"
12
13
  class Test::Unit::TestCase
13
14
 
14
15
  end
16
+
17
+ def create_database
18
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
19
+ ActiveRecord::Migration.verbose = false
20
+ CreateDb.migrate(:up)
21
+ end
@@ -1,14 +1,7 @@
1
1
  require "helper"
2
2
  require 'active_support/core_ext/module/aliasing'
3
3
 
4
- class CreateTrafficLights < ActiveRecord::Migration
5
- def self.up
6
- create_table(:traffic_lights) do |t|
7
- t.string :state
8
- t.string :name
9
- end
10
- end
11
- end
4
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
12
5
 
13
6
  class TrafficLight < ActiveRecord::Base
14
7
  include ActiveRecord::Transitions
@@ -52,10 +45,7 @@ end
52
45
 
53
46
  class TestActiveRecord < Test::Unit::TestCase
54
47
  def setup
55
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
56
- ActiveRecord::Migration.verbose = false
57
- CreateTrafficLights.migrate(:up)
58
-
48
+ create_database
59
49
  @light = TrafficLight.create!
60
50
  end
61
51
 
@@ -142,6 +132,7 @@ end
142
132
  class TestNewActiveRecord < TestActiveRecord
143
133
 
144
134
  def setup
135
+ create_database
145
136
  @light = TrafficLight.new
146
137
  end
147
138
 
@@ -0,0 +1,118 @@
1
+ require "helper"
2
+ require 'active_support/core_ext/module/aliasing'
3
+
4
+ create_database
5
+
6
+ class Order < ActiveRecord::Base
7
+ include ActiveRecord::Transitions
8
+
9
+ state_machine do
10
+ state :opened
11
+ state :placed
12
+ state :paid
13
+ state :prepared
14
+ state :delivered
15
+ state :cancelled
16
+
17
+ # no timestamp col is being specified here - should be ignored
18
+ event :place do
19
+ transitions :from => :opened, :to => :placed
20
+ end
21
+
22
+ # should set paid_at timestamp
23
+ event :pay, :timestamp => true do
24
+ transitions :from => :placed, :to => :paid
25
+ end
26
+
27
+ # should set prepared_on
28
+ event :prepare, :timestamp => true do
29
+ transitions :from => :paid, :to => :prepared
30
+ end
31
+
32
+ # should set dispatched_at
33
+ event :deliver, :timestamp => "dispatched_at" do
34
+ transitions :from => :prepared, :to => :delivered
35
+ end
36
+
37
+ # should set cancellation_date
38
+ event :cancel, :timestamp => :cancellation_date do
39
+ transitions :from => [:placed, :paid, :prepared], :to => :cancelled
40
+ end
41
+
42
+ # should raise an exception as there is no timestamp col
43
+ event :reopen, :timestamp => true do
44
+ transitions :from => :cancelled, :to => :opened
45
+ end
46
+
47
+ end
48
+ end
49
+
50
+
51
+ class TestActiveRecordTimestamps < Test::Unit::TestCase
52
+
53
+ require "securerandom"
54
+
55
+ def setup
56
+ create_database
57
+ end
58
+
59
+ def create_order(state = nil)
60
+ Order.create! order_number: SecureRandom.hex(4), state: state
61
+ end
62
+
63
+ # control case, no timestamp has been set so we should expect default behaviour
64
+ test "moving to placed does not raise any exceptions" do
65
+ @order = create_order
66
+ assert_nothing_raised { @order.place! }
67
+ assert_equal @order.state, "placed"
68
+ end
69
+
70
+ test "moving to paid should set paid_at" do
71
+ @order = create_order(:placed)
72
+ @order.pay!
73
+ @order.reload
74
+ assert_not_nil @order.paid_at
75
+ end
76
+
77
+ test "moving to prepared should set prepared_on" do
78
+ @order = create_order(:paid)
79
+ @order.prepare!
80
+ @order.reload
81
+ assert_not_nil @order.prepared_on
82
+ end
83
+
84
+ test "moving to delivered should set dispatched_at" do
85
+ @order = create_order(:prepared)
86
+ @order.deliver!
87
+ @order.reload
88
+ assert_not_nil @order.dispatched_at
89
+ end
90
+
91
+ test "moving to cancelled should set cancellation_date" do
92
+ @order = create_order(:placed)
93
+ @order.cancel!
94
+ @order.reload
95
+ assert_not_nil @order.cancellation_date
96
+ end
97
+
98
+ test "moving to reopened should raise an exception as there is no attribute" do
99
+ @order = create_order(:cancelled)
100
+ assert_raise(NoMethodError) { @order.re_open! }
101
+ @order.reload
102
+ end
103
+
104
+ test "passing an invalid value to timestamp options should raise an exception" do
105
+ assert_raise(ArgumentError) do
106
+ class Order < ActiveRecord::Base
107
+ include ActiveRecord::Transitions
108
+ state_machine do
109
+ event :replace, timestamp: 1 do
110
+ transitions :from => :prepared, :to => :placed
111
+ end
112
+ end
113
+
114
+ end
115
+ end
116
+ end
117
+
118
+ end
@@ -0,0 +1,64 @@
1
+ require "helper"
2
+ require 'active_support/core_ext/module/aliasing'
3
+
4
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
5
+
6
+ class CreateTrafficLights < ActiveRecord::Migration
7
+ def self.up
8
+ create_table(:traffic_lights) do |t|
9
+ t.string :state
10
+ t.string :name
11
+ end
12
+ end
13
+ end
14
+
15
+ class TrafficLight < ActiveRecord::Base
16
+ include ActiveRecord::Transitions
17
+
18
+ state_machine do
19
+ state :off
20
+
21
+ state :red
22
+ state :green
23
+ state :yellow
24
+
25
+ event :red_on do
26
+ transitions :to => :red, :from => [:yellow]
27
+ end
28
+
29
+ event :green_on do
30
+ transitions :to => :green, :from => [:red]
31
+ end
32
+
33
+ event :yellow_on do
34
+ transitions :to => :yellow, :from => [:green]
35
+ end
36
+
37
+ event :reset do
38
+ transitions :to => :red, :from => [:off]
39
+ end
40
+ end
41
+ end
42
+
43
+ class TestScopes < Test::Unit::TestCase
44
+ def setup
45
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
46
+ ActiveRecord::Migration.verbose = false
47
+ CreateTrafficLights.migrate(:up)
48
+
49
+ @light = TrafficLight.create!
50
+ end
51
+
52
+ test "scope returns correct object" do
53
+ assert TrafficLight.respond_to? :off
54
+ assert_equal TrafficLight.off.first, @light
55
+ assert TrafficLight.red.empty?
56
+ end
57
+
58
+ test "scopes exist" do
59
+ assert TrafficLight.respond_to? :off
60
+ assert TrafficLight.respond_to? :red
61
+ assert TrafficLight.respond_to? :green
62
+ assert TrafficLight.respond_to? :yellow
63
+ end
64
+ end
data/transitions.gemspec CHANGED
@@ -5,8 +5,8 @@ Gem::Specification.new do |s|
5
5
  s.name = "transitions"
6
6
  s.version = Transitions::VERSION
7
7
  s.platform = Gem::Platform::RUBY
8
- s.authors = ["Jakub Kuźma"]
9
- s.email = "qoobaa@gmail.com"
8
+ s.authors = ["Jakub Kuźma", "Timo Rößner"]
9
+ s.email = "timo.roessner@googlemail.com"
10
10
  s.homepage = "http://github.com/qoobaa/transitions"
11
11
  s.summary = "State machine extracted from ActiveModel"
12
12
  s.description = "Lightweight state machine extracted from ActiveModel"
metadata CHANGED
@@ -1,82 +1,78 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: transitions
3
- version: !ruby/object:Gem::Version
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.11
4
5
  prerelease:
5
- version: 0.0.10
6
6
  platform: ruby
7
- authors:
8
- - "Jakub Ku\xC5\xBAma"
7
+ authors:
8
+ - Jakub Kuźma
9
+ - Timo Rößner
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
-
13
- date: 2011-08-11 00:00:00 +02:00
14
- default_executable:
15
- dependencies:
16
- - !ruby/object:Gem::Dependency
13
+ date: 2011-10-12 00:00:00.000000000Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
17
16
  name: bundler
18
- requirement: &id001 !ruby/object:Gem::Requirement
17
+ requirement: &69475640 !ruby/object:Gem::Requirement
19
18
  none: false
20
- requirements:
19
+ requirements:
21
20
  - - ~>
22
- - !ruby/object:Gem::Version
23
- version: "1"
21
+ - !ruby/object:Gem::Version
22
+ version: '1'
24
23
  type: :development
25
24
  prerelease: false
26
- version_requirements: *id001
27
- - !ruby/object:Gem::Dependency
25
+ version_requirements: *69475640
26
+ - !ruby/object:Gem::Dependency
28
27
  name: test-unit
29
- requirement: &id002 !ruby/object:Gem::Requirement
28
+ requirement: &69470060 !ruby/object:Gem::Requirement
30
29
  none: false
31
- requirements:
30
+ requirements:
32
31
  - - ~>
33
- - !ruby/object:Gem::Version
34
- version: "2"
32
+ - !ruby/object:Gem::Version
33
+ version: '2'
35
34
  type: :development
36
35
  prerelease: false
37
- version_requirements: *id002
38
- - !ruby/object:Gem::Dependency
36
+ version_requirements: *69470060
37
+ - !ruby/object:Gem::Dependency
39
38
  name: mocha
40
- requirement: &id003 !ruby/object:Gem::Requirement
39
+ requirement: &69469200 !ruby/object:Gem::Requirement
41
40
  none: false
42
- requirements:
43
- - - ">="
44
- - !ruby/object:Gem::Version
45
- version: "0"
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
46
45
  type: :development
47
46
  prerelease: false
48
- version_requirements: *id003
49
- - !ruby/object:Gem::Dependency
47
+ version_requirements: *69469200
48
+ - !ruby/object:Gem::Dependency
50
49
  name: sqlite3-ruby
51
- requirement: &id004 !ruby/object:Gem::Requirement
50
+ requirement: &69468690 !ruby/object:Gem::Requirement
52
51
  none: false
53
- requirements:
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- version: "0"
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
57
56
  type: :development
58
57
  prerelease: false
59
- version_requirements: *id004
60
- - !ruby/object:Gem::Dependency
58
+ version_requirements: *69468690
59
+ - !ruby/object:Gem::Dependency
61
60
  name: activerecord
62
- requirement: &id005 !ruby/object:Gem::Requirement
61
+ requirement: &69467660 !ruby/object:Gem::Requirement
63
62
  none: false
64
- requirements:
63
+ requirements:
65
64
  - - ~>
66
- - !ruby/object:Gem::Version
67
- version: "3"
65
+ - !ruby/object:Gem::Version
66
+ version: '3'
68
67
  type: :development
69
68
  prerelease: false
70
- version_requirements: *id005
69
+ version_requirements: *69467660
71
70
  description: Lightweight state machine extracted from ActiveModel
72
- email: qoobaa@gmail.com
71
+ email: timo.roessner@googlemail.com
73
72
  executables: []
74
-
75
73
  extensions: []
76
-
77
74
  extra_rdoc_files: []
78
-
79
- files:
75
+ files:
80
76
  - .gitignore
81
77
  - Gemfile
82
78
  - Gemfile.lock
@@ -90,47 +86,45 @@ files:
90
86
  - lib/transitions/state.rb
91
87
  - lib/transitions/state_transition.rb
92
88
  - lib/transitions/version.rb
89
+ - test/db/create_db.rb
93
90
  - test/helper.rb
94
91
  - test/test_active_record.rb
92
+ - test/test_active_record_timestamps.rb
95
93
  - test/test_event.rb
96
94
  - test/test_event_arguments.rb
97
95
  - test/test_event_being_fired.rb
98
96
  - test/test_machine.rb
97
+ - test/test_scopes.rb
99
98
  - test/test_state.rb
100
99
  - test/test_state_transition.rb
101
100
  - test/test_state_transition_callbacks.rb
102
101
  - test/test_state_transition_guard_check.rb
103
102
  - transitions.gemspec
104
- has_rdoc: true
105
103
  homepage: http://github.com/qoobaa/transitions
106
104
  licenses: []
107
-
108
105
  post_install_message:
109
106
  rdoc_options: []
110
-
111
- require_paths:
107
+ require_paths:
112
108
  - lib
113
- required_ruby_version: !ruby/object:Gem::Requirement
109
+ required_ruby_version: !ruby/object:Gem::Requirement
114
110
  none: false
115
- requirements:
116
- - - ">="
117
- - !ruby/object:Gem::Version
118
- hash: 347067155
119
- segments:
111
+ requirements:
112
+ - - ! '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ segments:
120
116
  - 0
121
- version: "0"
122
- required_rubygems_version: !ruby/object:Gem::Requirement
117
+ hash: 59531697
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
119
  none: false
124
- requirements:
125
- - - ">="
126
- - !ruby/object:Gem::Version
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
127
123
  version: 1.3.6
128
124
  requirements: []
129
-
130
125
  rubyforge_project: transitions
131
- rubygems_version: 1.6.2
126
+ rubygems_version: 1.8.6
132
127
  signing_key:
133
128
  specification_version: 3
134
129
  summary: State machine extracted from ActiveModel
135
130
  test_files: []
136
-