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