Merge branches 'aasm_update' and 'master'

This commit is contained in:
Reinier Balt 2011-06-11 23:36:23 +02:00
commit 626edb478d
29 changed files with 115 additions and 860 deletions

View file

@ -89,9 +89,6 @@ class TodosController < ApplicationController
specified_state = @todo.state
@saved = @todo.save
# Fix for #977 because AASM overrides @state on creation
@todo.update_attribute('state', specified_state) unless specified_state == "immediate" || specified_state.nil? || !@saved
@saved = @todo.save
@todo.update_state_from_project if @saved
unless (@saved == false) || tag_list.blank?

View file

@ -40,23 +40,27 @@ class Project < ActiveRecord::Base
validates_does_not_contain :name, :string => ','
acts_as_list :scope => 'user_id = #{user_id} AND state = \'#{state}\''
acts_as_state_machine :initial => :active, :column => 'state'
include AASM
aasm_column :state
aasm_initial_state :active
extend NamePartFinder
#include Tracks::TodoList
state :active
state :hidden, :enter => :hide_todos, :exit => :unhide_todos
state :completed, :enter => Proc.new { |p| p.completed_at = Time.zone.now }, :exit => Proc.new { |p| p.completed_at = nil }
aasm_state :active
aasm_state :hidden, :enter => :hide_todos, :exit => :unhide_todos
aasm_state :completed, :enter => :set_completed_at_date, :exit => :clear_completed_at_date
event :activate do
transitions :to => :active, :from => [:hidden, :completed]
aasm_event :activate do
transitions :to => :active, :from => [:active, :hidden, :completed]
end
event :hide do
aasm_event :hide do
transitions :to => :hidden, :from => [:active, :completed]
end
event :complete do
aasm_event :complete do
transitions :to => :completed, :from => [:active, :hidden]
end
@ -92,6 +96,14 @@ class Project < ActiveRecord::Base
end
end
def set_completed_at_date
self.completed_at = Time.zone.now
end
def clear_completed_at_date
self.completed_at = nil
end
def note_count
cached_note_count || notes.count
end
@ -106,7 +118,7 @@ class Project < ActiveRecord::Base
# as a result of acts_as_state_machine calling state=() to update the attribute
def transition_to(candidate_state)
case candidate_state.to_sym
when current_state
when aasm_current_state
return
when :hidden
hide!

View file

@ -10,20 +10,19 @@ class RecurringTodo < ActiveRecord::Base
named_scope :completed, :conditions => { :state => 'completed'}
attr_protected :user
acts_as_state_machine :initial => :active, :column => 'state'
state :active, :enter => Proc.new { |t|
t[:show_from], t.completed_at = nil, nil
t.occurences_count = 0
}
state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
include AASM
aasm_column :state
aasm_initial_state :active
aasm_state :active, :enter => Proc.new { |t| t.occurences_count = 0 }
aasm_state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
event :complete do
aasm_event :complete do
transitions :to => :completed, :from => [:active]
end
event :activate do
aasm_event :activate do
transitions :to => :active, :from => [:completed]
end
@ -41,8 +40,7 @@ class RecurringTodo < ActiveRecord::Base
validate :set_recurrence_on_validations
def period_specific_validations
periods = %W[daily weekly monthly yearly]
if periods.include?(recurring_period)
if %W[daily weekly monthly yearly].include?(recurring_period)
self.send("validate_#{recurring_period}")
else
errors.add(:recurring_period, "is an unknown recurrence pattern: '#{self.recurring_period}'")
@ -94,7 +92,6 @@ class RecurringTodo < ActiveRecord::Base
end
end
def starts_and_ends_on_validations
errors.add_to_base("The start date needs to be filled in") if start_from.nil? || start_from.blank?
case self.ends_on

View file

@ -45,40 +45,42 @@ class Todo < ActiveRecord::Base
RE_PARTS = /'(#{RE_TODO})'\s<'(#{RE_CONTEXT})';\s'(#{RE_PROJECT})'>/ # results in array
RE_SPEC = /'#{RE_TODO}'\s<'#{RE_CONTEXT}';\s'#{RE_PROJECT}'>/ # results in string
acts_as_state_machine :initial => :active, :column => 'state'
include AASM
aasm_column :state
aasm_initial_state Proc.new { |todo| (todo.show_from && (todo.show_from > todo.user.date)) ? :deferred : :active}
# when entering active state, also remove completed_at date. Looks like :exit
# of state completed is not run, see #679
state :active, :enter => Proc.new { |t| t[:show_from], t.completed_at = nil, nil }
state :project_hidden
state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
state :deferred
state :pending
aasm_state :active
aasm_state :project_hidden, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
aasm_state :completed, :exit => Proc.new { |t| t.completed_at = nil }
aasm_state :deferred, :exit => Proc.new { |t| t[:show_from] = nil }
aasm_state :pending
event :defer do
aasm_event :defer do
transitions :to => :deferred, :from => [:active]
end
event :complete do
aasm_event :complete do
transitions :to => :completed, :from => [:active, :project_hidden, :deferred]
end
event :activate do
aasm_event :activate do
transitions :to => :active, :from => [:project_hidden, :completed, :deferred]
transitions :to => :active, :from => [:pending], :guard => :no_uncompleted_predecessors_or_deferral?
transitions :to => :deferred, :from => [:pending], :guard => :no_uncompleted_predecessors?
end
event :hide do
aasm_event :hide do
transitions :to => :project_hidden, :from => [:active, :deferred]
end
event :unhide do
aasm_event :unhide do
transitions :to => :deferred, :from => [:project_hidden], :guard => Proc.new{|t| !t.show_from.blank? }
transitions :to => :active, :from => [:project_hidden]
end
event :block do
aasm_event :block do
transitions :to => :pending, :from => [:active, :deferred]
end
@ -97,7 +99,7 @@ class Todo < ActiveRecord::Base
@predecessor_array = nil # Used for deferred save of predecessors
@removed_predecessors = nil
end
def no_uncompleted_predecessors_or_deferral?
return (show_from.blank? or Time.zone.now > show_from and uncompleted_predecessors.empty?)
end
@ -153,10 +155,8 @@ class Todo < ActiveRecord::Base
end
def remove_predecessor(predecessor)
puts "@@@ before delete"
# remove predecessor and activate myself
self.predecessors.delete(predecessor)
puts "@@@ before activate"
self.activate!
end
@ -214,9 +214,14 @@ class Todo < ActiveRecord::Base
def show_from=(date)
# parse Date objects into the proper timezone
date = user.at_midnight(date) if (date.is_a? Date)
# show_from needs to be set before state_change because of "bug" in aasm.
# If show_From is not set, the todo will not validate and thus aasm will not save
# (see http://stackoverflow.com/questions/682920/persisting-the-state-column-on-transition-using-rubyist-aasm-acts-as-state-machi)
self[:show_from] = date
activate! if deferred? && date.blank?
defer! if active? && !date.blank? && date > user.date
self[:show_from] = date
end
alias_method :original_project, :project
@ -224,27 +229,7 @@ class Todo < ActiveRecord::Base
def project
original_project.nil? ? Project.null_object : original_project
end
alias_method :original_set_initial_state, :set_initial_state
def set_initial_state
if show_from && (show_from > user.date)
write_attribute self.class.state_column, 'deferred'
else
original_set_initial_state
end
end
alias_method :original_run_initial_state_actions, :run_initial_state_actions
def run_initial_state_actions
# only run the initial state actions if the standard initial state hasn't
# been changed
if self.class.initial_state.to_sym == current_state
original_run_initial_state_actions
end
end
def self.feed_options(user)
{
:title => 'Tracks Actions',

View file

@ -17,7 +17,7 @@ suppress_edit_button ||= false
</div>
<div class="buttons">
<span class="grey"><%= project.current_state.to_s.upcase %></span>
<span class="grey"><%= project.aasm_current_state.to_s.upcase %></span>
<%= link_to_delete_project(project, image_tag( "blank.png", :title => t('projects.delete_project_title'), :class=>"delete_item")) %>
<%= suppress_edit_button ? "" : link_to_edit_project(project, image_tag( "blank.png", :title => t('projects.edit_project_title'), :class=>"edit_item")) %>
</div>

View file

@ -24,4 +24,4 @@
<% else -%><%= render :partial => "notes/mobile_notes_summary", :collection => @project.notes %>
<% end -%>
<h2><%= t('projects.settings') %></h2>
<%= t('projects.state', :state => project.current_state.to_s) %>. <%= @project_default_context %>
<%= t('projects.state', :state => project.aasm_current_state.to_s) %>. <%= @project_default_context %>

View file

@ -1,11 +1,11 @@
# TODO: is this dead code?
page.select('#project_status .active span').each do |element|
element.className = @project.current_state == :active ? 'active_state' : 'inactive_state'
element.className = @project.aasm_current_state == :active ? 'active_state' : 'inactive_state'
end
page.select('#project_status .hidden span').each do |element|
element.className = @project.current_state == :hidden ? 'active_state' : 'inactive_state'
element.className = @project.aasm_current_state == :hidden ? 'active_state' : 'inactive_state'
end
page.select('#project_status .completed span').each do |element|
element.className = @project.current_state == :completed ? 'active_state' : 'inactive_state'
element.className = @project.aasm_current_state == :completed ? 'active_state' : 'inactive_state'
end
page.notify :notice, "Set project status to #{@project.current_state}", 5.0
page.notify :notice, "Set project status to #{@project.aasm_current_state}", 5.0

View file

@ -29,6 +29,7 @@ Rails::Initializer.run do |config|
config.gem 'rack', :version => '1.1.0'
config.gem 'will_paginate', :version => '~> 2.3.15'
config.gem 'has_many_polymorphs'
config.gem 'aasm', :version => '2.2.0'
config.action_controller.use_accept_header = true

View file

@ -0,0 +1,13 @@
class AdaptToNewAasm < ActiveRecord::Migration
def self.up
change_column_default :todos, :state, nil
change_column_default :projects, :state, nil
change_column_default :recurring_todos, :state, nil
end
def self.down
change_column :todos, :state, :string, :limit => 20, :default => "immediate", :null => false
change_column :projects, :state, :string, :limit => 20, :default => "active", :null => false
change_column :recurring_todos, :state, :string, :limit => 20, :default => "active", :null => false
end
end

View file

@ -41,7 +41,7 @@ call_bill_gates_every_day:
show_from_delta: ~
recurring_period: daily
recurrence_selector: ~
show_always: 0
show_always: 1
every_other1: 1
every_other2: ~
every_other3: ~

View file

@ -58,7 +58,7 @@ class ProjectsControllerTest < TodoContainerControllerTestBase
login_as(:admin_user)
xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"hidden"}
todos.each do |t|
assert_equal :project_hidden, t.reload().current_state
assert_equal :project_hidden, t.reload().aasm_current_state
end
assert p.reload().hidden?
end
@ -70,7 +70,7 @@ class ProjectsControllerTest < TodoContainerControllerTestBase
xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"hidden"}
xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"active"}
todos.each do |t|
assert_equal :active, t.reload().current_state
assert_equal :active, t.reload().aasm_current_state
end
assert p.reload().active?
end

View file

@ -62,7 +62,6 @@ class TodosControllerTest < ActionController::TestCase
assert_equal 2, t.tags.count
end
def test_not_done_counts_after_hiding_project
p = Project.find(1)
p.hide!

View file

@ -53,26 +53,26 @@ class ProjectTest < ActiveSupport::TestCase
end
def test_project_initial_state_is_active
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
assert @timemachine.active?
end
def test_hide_project
@timemachine.hide!
assert_equal :hidden, @timemachine.current_state
assert_equal :hidden, @timemachine.aasm_current_state
assert @timemachine.hidden?
end
def test_activate_project
@timemachine.activate!
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
assert @timemachine.active?
end
def test_complete_project
assert_nil @timemachine.completed_at
@timemachine.complete!
assert_equal :completed, @timemachine.current_state
assert_equal :completed, @timemachine.aasm_current_state
assert @timemachine.completed?
assert_not_nil @timemachine.completed_at, "completed_at not expected to be nil"
assert_in_delta Time.now, @timemachine.completed_at, 1
@ -141,25 +141,26 @@ class ProjectTest < ActiveSupport::TestCase
end
def test_transition_to_another_state
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
@timemachine.transition_to(:hidden)
assert_equal :hidden, @timemachine.current_state
assert_equal :hidden, @timemachine.aasm_current_state
@timemachine.transition_to(:completed)
assert_equal :completed, @timemachine.current_state
assert_equal :completed, @timemachine.aasm_current_state
@timemachine.transition_to(:active)
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
end
def test_transition_to_same_state
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
@timemachine.transition_to(:active)
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
end
def test_deferred_todo_count
assert_equal 1, @timemachine.deferred_todos.count
assert_equal 0, @moremoney.deferred_todos.count
@moremoney.todos[0].show_from = next_week
assert_equal :deferred, @moremoney.todos[0].aasm_current_state
assert_equal 1, @moremoney.deferred_todos.count
end

View file

@ -275,25 +275,31 @@ class RecurringTodoTest < ActiveSupport::TestCase
end
def test_toggle_completion
t = @yearly
assert_equal :active, t.current_state
t.toggle_completion!
assert_equal :completed, t.current_state
t.toggle_completion!
assert_equal :active, t.current_state
assert @yearly.active?
assert @yearly.toggle_completion!
assert @yearly.completed?
# entering completed state should set completed_at
assert !@yearly.completed_at.nil?
assert @yearly.toggle_completion!
assert @yearly.active?
# re-entering active state should clear completed_at
assert @yearly.completed_at.nil?
end
def test_starred
@yearly.tag_with("1, 2, starred")
@yearly.tags.reload
assert_equal true, @yearly.starred?
assert_equal false, @weekly_every_day.starred?
assert @yearly.starred?
assert !@weekly_every_day.starred?
@yearly.toggle_star!
assert_equal false, @yearly.starred?
assert !@yearly.starred?
@yearly.toggle_star!
assert_equal true, @yearly.starred?
assert @yearly.starred?
end
def test_occurence_count
@ -307,8 +313,9 @@ class RecurringTodoTest < ActiveSupport::TestCase
# after completion, when you reactivate the recurring todo, the occurences
# count should be reset
assert_equal 2, @every_day.occurences_count
@every_day.toggle_completion!
@every_day.toggle_completion!
assert @every_day.toggle_completion!
assert @every_day.toggle_completion!
assert_equal true, @every_day.has_next_todo(@in_three_days)
assert_equal 0, @every_day.occurences_count
end

View file

@ -77,10 +77,10 @@ class TodoTest < ActiveSupport::TestCase
def test_defer_an_existing_todo
@not_completed2
assert_equal :active, @not_completed2.current_state
assert_equal :active, @not_completed2.aasm_current_state
@not_completed2.show_from = next_week
assert @not_completed2.save, "should have saved successfully" + @not_completed2.errors.to_xml
assert_equal :deferred, @not_completed2.current_state
assert_equal :deferred, @not_completed2.aasm_current_state
end
def test_create_a_new_deferred_todo
@ -88,16 +88,16 @@ class TodoTest < ActiveSupport::TestCase
todo = user.todos.build
todo.show_from = next_week
todo.context_id = 1
todo.description = 'foo'
todo.description = 'foo'
assert todo.save, "should have saved successfully" + todo.errors.to_xml
assert_equal :deferred, todo.current_state
assert_equal :deferred, todo.aasm_current_state
end
def test_create_a_new_deferred_todo_by_passing_attributes
user = users(:other_user)
todo = user.todos.build(:show_from => next_week, :context_id => 1, :description => 'foo')
todo = user.todos.build(:show_from => next_week, :context_id => 1, :description => 'foo')
assert todo.save, "should have saved successfully" + todo.errors.to_xml
assert_equal :deferred, todo.current_state
assert_equal :deferred, todo.aasm_current_state
end
def test_feed_options
@ -108,11 +108,11 @@ class TodoTest < ActiveSupport::TestCase
def test_toggle_completion
t = @not_completed1
assert_equal :active, t.current_state
assert_equal :active, t.aasm_current_state
t.toggle_completion!
assert_equal :completed, t.current_state
assert_equal :completed, t.aasm_current_state
t.toggle_completion!
assert_equal :active, t.current_state
assert_equal :active, t.aasm_current_state
end
def test_activate_also_saves
@ -154,7 +154,7 @@ class TodoTest < ActiveSupport::TestCase
t.context_id = 1
t.save!
t.reload
assert_equal :active, t.current_state
assert_equal :active, t.aasm_current_state
end
def test_initial_state_is_deferred_when_show_from_in_future
@ -165,7 +165,7 @@ class TodoTest < ActiveSupport::TestCase
t.show_from = 1.week.from_now.to_date
t.save!
t.reload
assert_equal :deferred, t.current_state
assert_equal :deferred, t.aasm_current_state
end
def test_todo_is_not_starred

View file

@ -1,13 +0,0 @@
* trunk *
break with true value [Kaspar Schiess]
* 2.1 *
After actions [Saimon Moore]
* 2.0 * (2006-01-20 15:26:28 -0500)
Enter / Exit actions
Transition guards
Guards and actions can be a symbol pointing to a method or a Proc
* 1.0 * (2006-01-15 12:16:55 -0500)
Initial Release

View file

@ -1,20 +0,0 @@
Copyright (c) 2006 Scott Barron
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,33 +0,0 @@
= Acts As State Machine
This act gives an Active Record model the ability to act as a finite state
machine (FSM).
Acquire via subversion at:
http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk
If prompted, use the user/pass anonymous/anonymous.
== Example
class Order < ActiveRecord::Base
acts_as_state_machine :initial => :opened
state :opened
state :closed, :enter => Proc.new {|o| Mailer.send_notice(o)}
state :returned
event :close do
transitions :to => :closed, :from => :opened
end
event :return do
transitions :to => :returned, :from => :closed
end
end
o = Order.create
o.close! # notice is sent by mailer
o.return!

View file

@ -1,28 +0,0 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => [:clean_db, :test]
desc 'Remove the stale db file'
task :clean_db do
`rm -f #{File.dirname(__FILE__)}/test/state_machine.sqlite.db`
end
desc 'Test the acts as state machine plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the acts as state machine plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'Acts As State Machine'
rdoc.options << '--line-numbers --inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('TODO')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -1,11 +0,0 @@
* Currently invalid events are ignored, create an option so that they can be
ignored or raise an exception.
* Query for a list of possible next states.
* Make listing states optional since they can be inferred from the events.
Only required to list a state if you want to define a transition block for it.
* Real transition actions
* Default states

View file

@ -1,5 +0,0 @@
require 'acts_as_state_machine'
ActiveRecord::Base.class_eval do
include ScottBarron::Acts::StateMachine
end

View file

@ -1,276 +0,0 @@
module ScottBarron #:nodoc:
module Acts #:nodoc:
module StateMachine #:nodoc:
class InvalidState < Exception #:nodoc:
end
class NoInitialState < Exception #:nodoc:
end
def self.included(base) #:nodoc:
base.extend ActMacro
end
module SupportingClasses
class State
attr_reader :name
def initialize(name, opts)
@name, @opts = name, opts
end
def entering(record)
enteract = @opts[:enter]
record.send(:run_transition_action, enteract) if enteract
end
def entered(record)
afteractions = @opts[:after]
return unless afteractions
Array(afteractions).each do |afteract|
record.send(:run_transition_action, afteract)
end
end
def exited(record)
exitact = @opts[:exit]
record.send(:run_transition_action, exitact) if exitact
end
end
class StateTransition
attr_reader :from, :to, :opts
def initialize(opts)
@from, @to, @guard = opts[:from], opts[:to], opts[:guard]
@opts = opts
end
def guard(obj)
@guard ? obj.send(:run_transition_action, @guard) : true
end
def perform(record)
return false unless guard(record)
loopback = record.current_state == to
states = record.class.read_inheritable_attribute(:states)
next_state = states[to]
old_state = states[record.current_state]
next_state.entering(record) unless loopback
if record.new_record?
record.send(record.class.state_column.to_s + '=', to.to_s)
else
record.update_attribute(record.class.state_column, to.to_s)
end
next_state.entered(record) unless loopback
old_state.exited(record) unless loopback
true
end
def ==(obj)
@from == obj.from && @to == obj.to
end
end
class Event
attr_reader :name
attr_reader :transitions
attr_reader :opts
def initialize(name, opts, transition_table, &block)
@name = name.to_sym
@transitions = transition_table[@name] = []
instance_eval(&block) if block
@opts = opts
@opts.freeze
@transitions.freeze
freeze
end
def next_states(record)
@transitions.select { |t| t.from == record.current_state }
end
def fire(record)
next_states(record).each do |transition|
break true if transition.perform(record)
end
end
def transitions(trans_opts)
Array(trans_opts[:from]).each do |s|
@transitions << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
end
end
end
end
module ActMacro
# Configuration options are
#
# * +column+ - specifies the column name to use for keeping the state (default: state)
# * +initial+ - specifies an initial state for newly created objects (required)
def acts_as_state_machine(opts)
self.extend(ClassMethods)
raise NoInitialState unless opts[:initial]
write_inheritable_attribute :states, {}
write_inheritable_attribute :initial_state, opts[:initial]
write_inheritable_attribute :transition_table, {}
write_inheritable_attribute :event_table, {}
write_inheritable_attribute :state_column, opts[:column] || 'state'
class_inheritable_reader :initial_state
class_inheritable_reader :state_column
class_inheritable_reader :transition_table
class_inheritable_reader :event_table
self.send(:include, ScottBarron::Acts::StateMachine::InstanceMethods)
before_create :set_initial_state
after_create :run_initial_state_actions
end
end
module InstanceMethods
def set_initial_state #:nodoc:
write_attribute self.class.state_column, self.class.initial_state.to_s
end
def run_initial_state_actions
initial = self.class.read_inheritable_attribute(:states)[self.class.initial_state.to_sym]
initial.entering(self)
initial.entered(self)
end
# Returns the current state the object is in, as a Ruby symbol.
def current_state
x = self.send(self.class.state_column)
return x.to_sym if not x.nil?
# if current state is not yet set, set it
self.set_initial_state
return self.current_state
end
# Returns what the next state for a given event would be, as a Ruby symbol.
def next_state_for_event(event)
ns = next_states_for_event(event)
ns.empty? ? nil : ns.first.to
end
def next_states_for_event(event)
self.class.read_inheritable_attribute(:transition_table)[event.to_sym].select do |s|
s.from == current_state
end
end
def run_transition_action(action)
Symbol === action ? self.method(action).call : action.call(self)
end
private :run_transition_action
end
module ClassMethods
# Returns an array of all known states.
def states
read_inheritable_attribute(:states).keys
end
# Define an event. This takes a block which describes all valid transitions
# for this event.
#
# Example:
#
# class Order < ActiveRecord::Base
# acts_as_state_machine :initial => :open
#
# state :open
# state :closed
#
# event :close_order do
# transitions :to => :closed, :from => :open
# end
# end
#
# +transitions+ takes a hash where <tt>:to</tt> is the state to transition
# to and <tt>:from</tt> is a state (or Array of states) from which this
# event can be fired.
#
# This creates an instance method used for firing the event. The method
# created is the name of the event followed by an exclamation point (!).
# Example: <tt>order.close_order!</tt>.
def event(event, opts={}, &block)
tt = read_inheritable_attribute(:transition_table)
et = read_inheritable_attribute(:event_table)
e = et[event.to_sym] = SupportingClasses::Event.new(event, opts, tt, &block)
define_method("#{event.to_s}!") { e.fire(self) }
end
# Define a state of the system. +state+ can take an optional Proc object
# which will be executed every time the system transitions into that
# state. The proc will be passed the current object.
#
# Example:
#
# class Order < ActiveRecord::Base
# acts_as_state_machine :initial => :open
#
# state :open
# state :closed, Proc.new { |o| Mailer.send_notice(o) }
# end
def state(name, opts={})
state = SupportingClasses::State.new(name.to_sym, opts)
read_inheritable_attribute(:states)[name.to_sym] = state
define_method("#{state.name}?") { current_state == state.name }
end
# Wraps ActiveRecord::Base.find to conveniently find all records in
# a given state. Options:
#
# * +number+ - This is just :first or :all from ActiveRecord +find+
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +find+
def find_in_state(number, state, *args)
with_state_scope state do
find(number, *args)
end
end
# Wraps ActiveRecord::Base.count to conveniently count all records in
# a given state. Options:
#
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +find+
def count_in_state(state, *args)
with_state_scope state do
count(*args)
end
end
# Wraps ActiveRecord::Base.calculate to conveniently calculate all records in
# a given state. Options:
#
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +calculate+
def calculate_in_state(state, *args)
with_state_scope state do
calculate(*args)
end
end
protected
def with_state_scope(state)
raise InvalidState unless states.include?(state)
with_scope :find => {:conditions => ["#{table_name}.#{state_column} = ?", state.to_s]} do
yield if block_given?
end
end
end
end
end
end

View file

@ -1,224 +0,0 @@
require File.dirname(__FILE__) + '/test_helper'
include ScottBarron::Acts::StateMachine
class ActsAsStateMachineTest < Test::Unit::TestCase
fixtures :conversations
def test_no_initial_value_raises_exception
assert_raise(NoInitialState) {
Person.acts_as_state_machine({})
}
end
def test_initial_state_value
assert_equal :needs_attention, Conversation.initial_state
end
def test_column_was_set
assert_equal 'state_machine', Conversation.state_column
end
def test_initial_state
c = Conversation.create
assert_equal :needs_attention, c.current_state
assert c.needs_attention?
end
def test_states_were_set
[:needs_attention, :read, :closed, :awaiting_response, :junk].each do |s|
assert Conversation.states.include?(s)
end
end
def test_event_methods_created
c = Conversation.create
%w(new_message! view! reply! close! junk! unjunk!).each do |event|
assert c.respond_to?(event)
end
end
def test_query_methods_created
c = Conversation.create
%w(needs_attention? read? closed? awaiting_response? junk?).each do |event|
assert c.respond_to?(event)
end
end
def test_transition_table
tt = Conversation.transition_table
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :read, :to => :needs_attention))
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :closed, :to => :needs_attention))
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :awaiting_response, :to => :needs_attention))
end
def test_next_state_for_event
c = Conversation.create
assert_equal :read, c.next_state_for_event(:view)
end
def test_change_state
c = Conversation.create
c.view!
assert c.read?
end
def test_can_go_from_read_to_closed_because_guard_passes
c = Conversation.create
c.can_close = true
c.view!
c.reply!
c.close!
assert_equal :closed, c.current_state
end
def test_cannot_go_from_read_to_closed_because_of_guard
c = Conversation.create
c.can_close = false
c.view!
c.reply!
c.close!
assert_equal :read, c.current_state
end
def test_ignore_invalid_events
c = Conversation.create
c.view!
c.junk!
# This is the invalid event
c.new_message!
assert_equal :junk, c.current_state
end
def test_entry_action_executed
c = Conversation.create
c.read_enter = false
c.view!
assert c.read_enter
end
def test_after_actions_executed
c = Conversation.create
c.read_after_first = false
c.read_after_second = false
c.closed_after = false
c.view!
assert c.read_after_first
assert c.read_after_second
c.can_close = true
c.close!
assert c.closed_after
assert_equal :closed, c.current_state
end
def test_after_actions_not_run_on_loopback_transition
c = Conversation.create
c.view!
c.read_after_first = false
c.read_after_second = false
c.view!
assert !c.read_after_first
assert !c.read_after_second
c.can_close = true
c.close!
c.closed_after = false
c.close!
assert !c.closed_after
end
def test_exit_action_executed
c = Conversation.create
c.read_exit = false
c.view!
c.junk!
assert c.read_exit
end
def test_entry_and_exit_not_run_on_loopback_transition
c = Conversation.create
c.view!
c.read_enter = false
c.read_exit = false
c.view!
assert !c.read_enter
assert !c.read_exit
end
def test_entry_and_after_actions_called_for_initial_state
c = Conversation.create
assert c.needs_attention_enter
assert c.needs_attention_after
end
def test_run_transition_action_is_private
c = Conversation.create
assert_raise(NoMethodError) { c.run_transition_action :foo }
end
def test_find_all_in_state
cs = Conversation.find_in_state(:all, :read)
assert_equal 2, cs.size
end
def test_find_first_in_state
c = Conversation.find_in_state(:first, :read)
assert_equal conversations(:first).id, c.id
end
def test_find_all_in_state_with_conditions
cs = Conversation.find_in_state(:all, :read, :conditions => ['subject = ?', conversations(:second).subject])
assert_equal 1, cs.size
assert_equal conversations(:second).id, cs.first.id
end
def test_find_first_in_state_with_conditions
c = Conversation.find_in_state(:first, :read, :conditions => ['subject = ?', conversations(:second).subject])
assert_equal conversations(:second).id, c.id
end
def test_count_in_state
cnt0 = Conversation.count(['state_machine = ?', 'read'])
cnt = Conversation.count_in_state(:read)
assert_equal cnt0, cnt
end
def test_count_in_state_with_conditions
cnt0 = Conversation.count(['state_machine = ? AND subject = ?', 'read', 'Foo'])
cnt = Conversation.count_in_state(:read, ['subject = ?', 'Foo'])
assert_equal cnt0, cnt
end
def test_find_in_invalid_state_raises_exception
assert_raise(InvalidState) {
Conversation.find_in_state(:all, :dead)
}
end
def test_count_in_invalid_state_raises_exception
assert_raise(InvalidState) {
Conversation.count_in_state(:dead)
}
end
def test_can_access_events_via_event_table
event = Conversation.event_table[:junk]
assert_equal :junk, event.name
assert_equal "finished", event.opts[:note]
end
end

View file

@ -1,18 +0,0 @@
sqlite:
:adapter: sqlite
:dbfile: state_machine.sqlite.db
sqlite3:
:adapter: sqlite3
:dbfile: state_machine.sqlite3.db
postgresql:
:adapter: postgresql
:username: postgres
:password: postgres
:database: state_machine_test
:min_messages: ERROR
mysql:
:adapter: mysql
:host: localhost
:username: rails
:password:
:database: state_machine_test

View file

@ -1,67 +0,0 @@
class Conversation < ActiveRecord::Base
attr_writer :can_close
attr_accessor :read_enter, :read_exit, :read_after_first, :read_after_second,
:closed_after, :needs_attention_enter, :needs_attention_after
acts_as_state_machine :initial => :needs_attention, :column => 'state_machine'
state :needs_attention, :enter => Proc.new { |o| o.needs_attention_enter = true },
:after => Proc.new { |o| o.needs_attention_after = true }
state :read, :enter => :read_enter_action,
:exit => Proc.new { |o| o.read_exit = true },
:after => [:read_after_first_action, :read_after_second_action]
state :closed, :after => :closed_after_action
state :awaiting_response
state :junk
event :new_message do
transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
end
event :view do
transitions :to => :read, :from => [:needs_attention, :read]
end
event :reply do
transitions :to => :awaiting_response, :from => [:read, :closed]
end
event :close do
transitions :to => :closed, :from => [:read, :awaiting_response], :guard => Proc.new {|o| o.can_close?}
transitions :to => :read, :from => [:read, :awaiting_response], :guard => :always_true
end
event :junk, :note => "finished" do
transitions :to => :junk, :from => [:read, :closed, :awaiting_response]
end
event :unjunk do
transitions :to => :closed, :from => :junk
end
def can_close?
@can_close
end
def read_enter_action
self.read_enter = true
end
def always_true
true
end
def read_after_first_action
self.read_after_first = true
end
def read_after_second_action
self.read_after_second = true
end
def closed_after_action
self.closed_after = true
end
end

View file

@ -1,11 +0,0 @@
first:
id: 1
state_machine: read
subject: This is a test
closed: false
second:
id: 2
state_machine: read
subject: Foo
closed: false

View file

@ -1,2 +0,0 @@
class Person < ActiveRecord::Base
end

View file

@ -1,11 +0,0 @@
ActiveRecord::Schema.define(:version => 1) do
create_table :conversations, :force => true do |t|
t.column :state_machine, :string
t.column :subject, :string
t.column :closed, :boolean
end
create_table :people, :force => true do |t|
t.column :name, :string
end
end

View file

@ -1,38 +0,0 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
RAILS_ROOT = File.dirname(__FILE__)
require 'rubygems'
require 'test/unit'
require 'active_record'
require 'active_record/fixtures'
require 'active_support/binding_of_caller'
require 'active_support/breakpoint'
require "#{File.dirname(__FILE__)}/../init"
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
load(File.dirname(__FILE__) + "/schema.rb") if File.exist?(File.dirname(__FILE__) + "/schema.rb")
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
class Test::Unit::TestCase #:nodoc:
def create_fixtures(*table_names)
if block_given?
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
else
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
end
end
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
end