Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for configurable column names #73

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,20 @@ You can easily get a listing of all available states:
state :closed
end

=== Configuring a different column name with ActiveRecord

To use a different column than <tt>state</tt> to track it's value simply do this:

class Product < ActiveRecord::Base
include Transitions

state_machine :attribute_name => :different_column do

...

end
end

=== Documentation, Guides & Examples

- {Online API Documentation}[http://rdoc.info/github/troessner/transitions/master/Transitions]
Expand Down
47 changes: 39 additions & 8 deletions lib/active_model/transitions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,31 @@ module Transitions
extend ActiveSupport::Concern

included do
class ::Transitions::Machine
unless instance_methods.include?(:new_initialize) || instance_methods.include?(:new_update)
attr_reader :attribute_name
alias :old_initialize :initialize
alias :old_update :update

def new_initialize(*args, &block)
@attribute_name = :state
old_initialize(*args, &block)
end

def new_update(options = {}, &block)
@attribute_name = options[:attribute_name] if options.key?(:attribute_name)
old_update(options, &block)
end

alias :initialize :new_initialize
alias :update :new_update
else
puts "WARNING: Transitions::Machine#new_update or Transitions::Machine#new_initialize already defined. This can possibly break ActiveModel::Transitions."
end
end
include ::Transitions
after_initialize :set_initial_state
validates_presence_of :state
validate :state_presence
validate :state_inclusion
end

Expand All @@ -43,6 +65,10 @@ def reload(options = nil)

protected

def transitions_state_column_name
self.class.state_machine.attribute_name
end

def write_state(state)
prev_state = current_state
write_state_without_persistence(state)
Expand All @@ -55,22 +81,27 @@ def write_state(state)
def write_state_without_persistence(state)
ivar = self.class.get_state_machine.current_state_variable
instance_variable_set(ivar, state)
self.state = state.to_s
self[transitions_state_column_name] = state.to_s
end

def read_state
self.state && self.state.to_sym
self[transitions_state_column_name] && self[transitions_state_column_name].to_sym
end

def set_initial_state
self.state ||= self.class.get_state_machine.initial_state.to_s if self.respond_to?(:state=)
self[transitions_state_column_name] ||= self.class.get_state_machine.initial_state.to_s if self.has_attribute?(transitions_state_column_name)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be:

.....if self.respond_to?(transitions_state_column_name)

otherwise we would have a regression, see here for details: #68

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> Fixed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks. Didn't know about that issue. Interesting behavior and good to know. So, is there anything else I can help with or are we all set with this fix?

end

def state_presence
unless self[transitions_state_column_name].present?
self.errors.add(transitions_state_column_name, :presence)
end
end

def state_inclusion
unless self.class.get_state_machine.states.map{|s| s.name.to_s }.include?(self.state.to_s)
self.errors.add(:state, :inclusion, :value => self.state)
unless self.class.get_state_machine.states.map{|s| s.name.to_s }.include?(self[transitions_state_column_name].to_s)
self.errors.add(transitions_state_column_name, :inclusion, :value => self[transitions_state_column_name])
end
end
end
end

end
156 changes: 156 additions & 0 deletions test/active_record/test_active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ def self.up

set_up_db CreateTrafficLights

class CreateDifferentTrafficLights < ActiveRecord::Migration
def self.up
create_table(:different_traffic_lights) do |t|
t.string :different_state
t.string :name
end
end
end

set_up_db CreateDifferentTrafficLights

class TrafficLight < ActiveRecord::Base
include ActiveModel::Transitions

Expand Down Expand Up @@ -147,3 +158,148 @@ def setup
assert_equal "red", @light.reload.state
end
end

class TestNewActiveRecord < TestActiveRecord

def setup
set_up_db CreateTrafficLights
@light = TrafficLight.new
end

test "new active records defaults current state to the initial state" do
assert_equal :off, @light.current_state
end

end

class TestScopes < Test::Unit::TestCase
test "scope returns correct object" do
@light = TrafficLight.create!
assert_respond_to TrafficLight, :off
assert_equal TrafficLight.off.first, @light
assert TrafficLight.red.empty?
end

test "scopes exist" do
assert_respond_to TrafficLight, :off
assert_respond_to TrafficLight, :red
assert_respond_to TrafficLight, :green
assert_respond_to TrafficLight, :yellow
end

test 'scopes are only generated if we explicitly say so' do
assert_not_respond_to LightBulb, :off
assert_not_respond_to LightBulb, :on
end

test 'scope generation raises an exception if we try to overwrite an existing method' do
assert_raise(Transitions::InvalidMethodOverride) {
class Light < ActiveRecord::Base
include ActiveModel::Transitions

state_machine :auto_scopes => true do
state :new
state :broken
end
end
}
end
end

class DifferentTrafficLight < ActiveRecord::Base
include ActiveModel::Transitions

state_machine :attribute_name => :different_state, :auto_scopes => true do
state :off

state :red
state :green
state :yellow

event :red_on do
transitions :to => :red, :from => [:yellow]
end

event :green_on do
transitions :to => :green, :from => [:red]
end

event :yellow_on do
transitions :to => :yellow, :from => [:green]
end

event :reset do
transitions :to => :red, :from => [:off]
end
end
end

class TestActiveRecordWithDifferentColumnName < Test::Unit::TestCase
def setup
set_up_db CreateDifferentTrafficLights
@light = DifferentTrafficLight.create!
end

test "new record has the initial state set" do
@light = DifferentTrafficLight.new
assert_equal "off", @light.different_state
end

test "states initial state" do
assert @light.off?
assert_equal :off, @light.current_state
end

test "transition to a valid state" do
@light.reset
assert @light.red?
assert_equal :red, @light.current_state

@light.green_on
assert @light.green?
assert_equal :green, @light.current_state
end

test "transition does not persist state" do
@light.reset
assert_equal :red, @light.current_state
@light.reload
assert_equal "off", @light.different_state
end

test "transition does persists state" do
@light.reset!
assert_equal :red, @light.current_state
@light.reload
assert_equal "red", @light.different_state
end

test "transition to an invalid state" do
assert_raise(Transitions::InvalidTransition) { @light.yellow_on }
assert_equal :off, @light.current_state
end

test "transition with wrong state will not validate" do
for s in @light.class.state_machine.states
@light.different_state = s.name
assert @light.valid?
end
@light.different_state = "invalid_one"
assert_false @light.valid?
end

test "reloading model resets current state" do
@light.reset
assert @light.red?
@light.update_attribute(:different_state, 'green')
assert @light.reload.green?, "reloaded state should come from database, not instance variable"
end

test "calling non-bang event updates state attribute" do
@light.reset!
assert @light.red?
@light.green_on
assert_equal "green", @light.different_state
assert_equal "red", @light.reload.different_state
end
end