transitions 0.0.10 → 0.0.11

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