module Mongoid module Undo extend ActiveSupport::Concern included do include Mongoid::Document field :version, type: Integer, default: 0 before_save :prepare_versioning after_save :create_version before_destroy :version_destroy end def versions; ::Version.where(obj_id: id) end def versions_gt(version = self.version); versions.where(:version.gt => version) end def version_at(version = self.version); versions.where(version: version).first end def current_version; version_at end def previous_version; version_at version - 1 end def versioned?; version > 0 end def has?(ver) ver.obj_id == id && ver.obj_class == self.class.to_s end def matches?(ver) has?(ver) && ver.obj_attributes == attributes && ver.version == version end def undo!; rollback_to!(version - 1) end def rollback_to!(version) unless persisted?; return false end if version == 0 versions.destroy without_versioning :destroy attributes else ver = version_at(version) unless ver; return false end ver.rollback_to(self) end end def without_versioning(method = nil) @without_versioning = true method ? method.to_proc.call(self) : yield ensure @without_versioning = false end private def match_persisted? current_version && attributes == current_version.obj_attributes end def prepare_versioning if !@without_versioning if version == 0 && persisted? self.version += 1 old_self = self.class.find(id) old_self.version += 1 old_self.send(:create_version) end if !match_persisted?; self.version += 1 end end end def create_version(destroy = false) if !@without_versioning && (destroy || !match_persisted?) ver = ::Version.new(obj_id: id, obj_class: self.class.to_s, version: version) unless destroy; ver.attributes = ver.attributes.merge({:obj_attributes => attributes}) end ver.save! end end def version_destroy if !@without_versioning self.version += 1 without_versioning(:save) create_version(true) end end end end