module VestalVersions # The control feature allows use of several code blocks that provide finer control over whether # a new version is created, or a previous version is updated. module Control extend ActiveSupport::Concern included do class_attribute :_skip_version, :instance_writer => false end # Control blocks are called on ActiveRecord::Base instances as to not cause any conflict with # other instances of the versioned class whose behavior could be inadvertently altered within # a control block. module InstanceMethods # The +skip_version+ block simply allows for updates to be made to an instance of a versioned # ActiveRecord model while ignoring all new version creation. The :if and # :unless conditions (if given) will not be evaulated inside a +skip_version+ block. # # When the block closes, the instance is automatically saved, so explicitly saving the # object within the block is unnecessary. # # == Example # # user = User.find_by_first_name("Steve") # user.version # => 1 # user.skip_version do # user.first_name = "Stephen" # end # user.version # => 1 def skip_version _with_version_flag(:_skip_version) do yield if block_given? save end end # Behaving almost identically to the +skip_version+ block, the only difference with the # +skip_version!+ block is that the save automatically performed at the close of the block # is a +save!+, meaning that an exception will be raised if the object cannot be saved. def skip_version! _with_version_flag(:_skip_version) do yield if block_given? save! end end # Merging versions with the +merge_version+ block will take all of the versions that would # be created within the block and merge them into one version and pushing that single version # onto the ActiveRecord::Base instance's version history. A new version will be created and # the instance's version number will be incremented. # # == Example # # user = User.find_by_first_name("Steve") # user.version # => 1 # user.merge_version do # user.update_attributes(:first_name => "Steven", :last_name => "Tyler") # user.update_attribute(:first_name, "Stephen") # user.update_attribute(:last_name, "Richert") # end # user.version # => 2 # user.versions.last.changes # # => {"first_name" => ["Steve", "Stephen"], "last_name" => ["Jobs", "Richert"]} # # See VestalVersions::Changes for an explanation on how changes are appended. def merge_version _with_version_flag(:merge_version) do yield if block_given? end save end # Behaving almost identically to the +merge_version+ block, the only difference with the # +merge_version!+ block is that the save automatically performed at the close of the block # is a +save!+, meaning that an exception will be raised if the object cannot be saved. def merge_version! _with_version_flag(:merge_version) do yield if block_given? end save! end # A convenience method for determining whether a versioned instance is set to merge its next # versions into one before version creation. def merge_version? !!@merge_version end # Appending versions with the +append_version+ block acts similarly to the +merge_version+ # block in that all would-be version creations within the block are defered until the block # closes. The major difference is that with +append_version+, a new version is not created. # Rather, the cumulative changes are appended to the serialized changes of the instance's # last version. A new version is not created, so the version number is not incremented. # # == Example # # user = User.find_by_first_name("Steve") # user.version # => 2 # user.versions.last.changes # # => {"first_name" => ["Stephen", "Steve"]} # user.append_version do # user.last_name = "Jobs" # end # user.versions.last.changes # # => {"first_name" => ["Stephen", "Steve"], "last_name" => ["Richert", "Jobs"]} # user.version # => 2 # # See VestalVersions::Changes for an explanation on how changes are appended. def append_version _with_version_flag(:merge_version) do yield if block_given? end _with_version_flag(:append_version) do save end end # Behaving almost identically to the +append_version+ block, the only difference with the # +append_version!+ block is that the save automatically performed at the close of the block # is a +save!+, meaning that an exception will be raised if the object cannot be saved. def append_version! _with_version_flag(:merge_version) do yield if block_given? end _with_version_flag(:append_version) do save! end end # A convenience method for determining whether a versioned instance is set to append its next # version's changes into the last version changes. def append_version? !!@append_version end # Used for each control block, the +_with_version_flag+ method sets a given variable to # true and then executes the given block, ensuring that the variable is returned to a nil # value before returning. This is useful to be certain that one of the control flag # instance variables isn't inadvertently left in the "on" position by execution within the # block raising an exception. def _with_version_flag(flag) instance_variable_set("@#{flag}", true) yield ensure remove_instance_variable("@#{flag}") end # Overrides the basal +create_version?+ method to make sure that new versions are not # created when inside any of the control blocks (until the block terminates). def create_version? !_skip_version? && !merge_version? && !append_version? && super end # Overrides the basal +update_version?+ method to allow the last version of an versioned # ActiveRecord::Base instance to be updated at the end of an +append_version+ block. def update_version? append_version? end end module ClassMethods # The +skip_version+ block simply allows for updates to be made to an instance of a versioned # ActiveRecord model while ignoring all new version creation. The :if and # :unless conditions (if given) will not be evaulated inside a +skip_version+ block. # # When the block closes, the instance is automatically saved, so explicitly saving the # object within the block is unnecessary. # # == Example # # user = User.find_by_first_name("Steve") # user.version # => 1 # user.skip_version do # user.first_name = "Stephen" # end # user.version # => 1 def skip_version _with_version_flag(:_skip_version) do yield if block_given? end end # Used for each control block, the +with_version_flag+ method sets a given variable to # true and then executes the given block, ensuring that the variable is returned to a nil # value before returning. This is useful to be certain that one of the control flag # instance variables isn't inadvertently left in the "on" position by execution within the # block raising an exception. def _with_version_flag(flag) self.send("#{flag}=", true) yield ensure self.send("#{flag}=", nil) end end end end