module Consolvix
module TransactionAPI
protected
def transaction_started?
not @transaction.nil?
end
def cleanup_transaction
@transaction.destroy
@transaction = nil
end
def load_transaction
unless params[:tid].blank?
t = ConsolvixTransaction.find_by_number(params[:tid])
@transaction = t if t and not t.is_complete?
end
true # ensure this filter doesn't break the filter chain
end
def save_transaction
@transaction.save! if transaction_started?
end
def start_transaction(description, action, controller=nil)
controller ||= controller_name
@transaction = ConsolvixTransaction.new(:controller => controller,
:action => action.to_s,
:user_id => @current_user,
:description => description)
end
def complete_transaction
@transaction.complete = true if transaction_started?
save_transaction
end
end
class Application
# here we basically add [load_transaction] as a [before_filter] to the
# actions specified in args.
# args is an Array of action names (Symbols, not Strings!) that
# may use @transaction, the current [ConsolvixTransaction] object
# it is NOT NECESSARY to declare methods here that are given to
# [define_transaction_handler] for those actions are automagically
# being taken care of.
def self.load_transaction_for(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
options.merge! :only => args
unless args.empty?
class_eval do
before_filter :load_transaction, options
end
end
end
# This basically adds a [save_transaction] as an [after_filter]
# to the action specified in args.
# args is an Array of action names that potentially change the state
# of @transaction, the currently running [ConsolvixTransaction] object.
# it is NOT NECESSARY to declare methods here that are given to
# [define_transaction_handler] for those actions are automagically
# being taken care of.
def self.save_transaction_for(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
options.merge! :only => args
unless args.empty?
class_eval do
after_filter :save_transaction, options
end
end
end
def self.transaction_handler(description, main_action, *actions)
sub_actions = actions.last.is_a?(Hash) ? actions.pop : {}
worker_action = sub_actions.delete(:worker) || "do_#{main_action}".to_sym
selector_action = sub_actions.delete(:selector) || "select_transact_#{main_action}".to_sym
action_map = []
@transaction_descriptors ||= {}
sub_actions.each do |number, data|
action_map[number.to_i] = {:action => data[0], :prerequisite => data[1]}
end
@transaction_descriptors[main_action] = { :selector_action => selector_action,
:description => description,
:main_action => main_action,
:worker_action => worker_action,
:total_step_count => action_map.size - 1,
:action_map => action_map }
create_main_action_handler(@transaction_descriptors[main_action])
create_worker_action_handler(@transaction_descriptors[main_action])
create_tid_selection_handler(@transaction_descriptors[main_action])
create_transaction_handler_stubs(@transaction_descriptors[main_action])
# # FIXME: does not work when called repeadedly! (i.e. for n>1 transaction handlers per controller)
class_eval do
load_transaction_for main_action,
worker_action,
selector_action,
:abort_transaction
save_transaction_for main_action,
worker_action
end
end
# Abort the currently running transaction (identified by params[:tid]);
# Can be overridden in the implementing class for custom behaviour.
# However, the implementation below should do fine in most cases.
def abort_transaction
if transaction_started?
worker_action = transaction_descriptor[:worker_action]
cleanup_transaction # here, @transaction will be completely destroyed
flash[:notice] = 'Aborted, you can start again now.'
else
flash[:error] = 'No transaction ID provided, nothing aborted.'
worker_action = 'index'
end
redirect_to :controller => controller_name,
:action => worker_action
end
protected
# returnd the meta information for the currently active
# transaction that is stored as a singleton class variable
# of the controller
def transaction_descriptor
if transaction_started?
# retrun the descriptor for the currently running transaction
main_action = @transaction.action.to_sym
return self.class.instance_variable_get('@transaction_descriptors')[main_action]
else
# try to identify transaction metadata by finding out wether
# the currently called action is the worker or selector action
main_action, descriptor = self.class.instance_variable_get('@transaction_descriptors').find do |key, descr|
descr[:worker_action] == action_name.to_sym
end
if descriptor
return descriptor
else
raise "The called action #{action_name} does not belong to any Transaction!"
end
end
end
# define the main action for this transaction.
# In most cases, this should do fine and neets not be overwritten in a child class.
# However, if you do, remember to call [super] first before calling own operations!
# ...Or just remember to call [start_transaction] with the right parametres and then
# redirect to the worker action.
def self.create_main_action_handler(descriptor)
class_eval do
public
define_method descriptor[:main_action] do
# load_transaction
abort_transaction and return if request.delete?
start_transaction(descriptor[:description],
descriptor[:main_action],
controller_name) if @transaction.nil?
redirect_to(:controller => controller_name,
:action => descriptor[:worker_action],
:id => params[:id],
:step => 1,
:tid => @transaction.number)
# save_transaction
end
end
end
# This defines the main worker action that basically just
# acts as a proxy to [handle_transaction]
def self.create_worker_action_handler(descriptor)
class_eval do
public
define_method descriptor[:worker_action] do
# load_transaction
return send(:handle_transaction)
# save_transaction
end
end
end
# This defines a stub for the transaction selection action.
# It can be implemented in the child class, however it is sufficient
# if the views folder for the active controller contains a
# *.rhtml file named after the [selector_action] specified above.
def self.create_tid_selection_handler(descriptor)
class_eval do
define_method descriptor[:selector_action] do
# load_transaction
@open_transactions = ConsolvixTransaction.find_all_by_controller_and_action(controller_name, descriptor[:main_action].to_s).collect {|t| ["#{t.description} (#{t.number})", t.number]}
begin
render :action => descriptor[:selector_action], :layout => !request.xhr?
rescue
render :text => "Here you should be seeing a selector for currently running transactions for #{descriptor[:main_action]}. However, you don`t seem to have implemented it yet!
It is sufficient to create /app/views/#{controller_name}/#{descriptor[:selector_action]}) with a form that sends a valid `tid` to . Just create a select_tag that takes its values from @open_transactions"
end
end
end
end
def self.create_transaction_handler_stubs(descriptor)
descriptor[:action_map].each do |a|
next if a.nil?
case a[:action]
when descriptor[:main_action]
raise "Found transaction name `#{a[:action]}' to be one of the steps for transaction `#{descriptor[:main_action]}' in `#{controller_name}'. However, this is not allowed!\nAll prerequisite actions of a transaction should be implemented as private or protected methods in order for them not to be callable directly."
when descriptor[:worker_action]
raise "Found transaction interface name `#{a[:action]}' to be one of the steps for transaction `#{descriptor[:main_action]}' in `#{controller_name}'. However, this is not allowed!\nAll substeps of a transaction should be implemented as private or protected methods in order for them not to be callable directly."
end
create_step_method_stub(a)
create_prerequisite_method_stub(a) unless a[:prerequisite].nil?
end
end
# Here we define empty stub actions for all transaction steps.
# They must be implemented in the child class.
def self.create_step_method_stub(map)
class_eval do
define_method map[:action] do
render :text => "This is is an empty implementation of #{map[:action]}. Prerequisite #{map[:prerequisite]||'[NONE]'} is OK if you can read this. Please implement me now!" and return
end
end
end
# Let's just predefine all prerequisite actions.
# They also have to be overwritten in the child class.
# By default, they all return FALSE (i.e. prerequisite failed)
def self.create_prerequisite_method_stub(map)
class_eval do
define_method map[:prerequisite] do
return false
end
end
end
# returns the called step number or otherwise the highest
# step number available in this transaction
def requested_step_no
if params[:step].to_i <= last_step_of_transaction and params[:step].to_i > 0
return params[:step].to_i
else
return last_step_of_transaction
end
end
def last_step_of_transaction
transaction_descriptor[:total_step_count]
end
def redirect_to_next_if_step_done
if transaction_started?
prerequisite = transaction_descriptor[:action_map][requested_step_no+1][:prerequisite]
if prerequisite and send prerequisite
redirect_to :action => transaction_descriptor[:worker_action],
:step => last_step_of_transaction,
:id => params[:id],
:tid => @transaction.number
end
end
end
def handle_transaction
abort_transaction and return if request.delete?
if transaction_started?
# This incrementally checks the prerequisite for the step called.
# If the prerequisite is OK, the requested step is called.
# If not, forward to the highest step number whose prerequisite is fulfilled.
1.upto requested_step_no do |i|
action_map = transaction_descriptor[:action_map][i]
if i == requested_step_no and (action_map[:prerequisite].nil? or send(action_map[:prerequisite]))
return send(action_map[:action])
elsif action_map[:prerequisite] and not send(action_map[:prerequisite])
redirect_to :controller => controller_name,
:action => transaction_descriptor[:worker_action],
:id => params[:id],
:step => i-1,
:tid => @transaction.number and return
else
redirect_to :controller => controller_name,
:action => transaction_descriptor[:worker_action],
:id => params[:id],
:step => 1,
:tid => @transaction.number and return
end
end
else
# FIXME:
# @open_transactions = ConsolvixTransaction.find_all_by_controller_and_action_and_user_id(controller_name, 'cha_do_create', @current_user.id) || []
transaction_count = ConsolvixTransaction.count(:conditions => "controller='#{controller_name}' AND action='#{transaction_descriptor[:main_action]}'")
if transaction_count == 0
redirect_to :action => 'index' and return
else
# one or more transaction found, show selection
redirect_to :action => transaction_descriptor[:selector_action] and return
end
end
end
end
end