Module: ActiveRecord::Acts::Versioned

Defined in:
lib/acts_as_versioned.rb

Overview

Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version column is present as well.

The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart your container for the changes to be reflected. In development mode this usually means restarting WEBrick.

class Page < ActiveRecord::Base
  # assumes pages_versions table
  acts_as_versioned
end

Example:

page = Page.create(:title => 'hello world!')
page.version       # => 1

page.title = 'hello world'
page.save
page.version       # => 2
page.versions.size # => 2

page.revert_to(1)  # using version number
page.title         # => 'hello world!'

page.revert_to(page.versions.last) # using versioned instance
page.title         # => 'hello world'

page.versions.earliest # efficient query to find the first version
page.versions.latest   # efficient query to find the most recently created version

Simple Queries to page between versions

page.versions.before(version) 
page.versions.after(version)

Access the previous/next versions from the versioned model itself

version = page.versions.latest
version.previous # go back one version
version.next     # go forward one version

See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options

Defined Under Namespace

Modules: Behaviors

Constant Summary collapse

VERSION =
"0.6.0"
CALLBACKS =
[:set_new_version, :save_version, :save_version?]

Instance Method Summary collapse

Instance Method Details

#acts_as_versioned(options = {}, &extension) ⇒ Object

Configuration options

  • class_name - versioned model class name (default: PageVersion in the above example)

  • table_name - versioned model table name (default: page_versions in the above example)

  • foreign_key - foreign key used to relate the versioned model to the original model (default: page_id in the above example)

  • inheritance_column - name of the column to save the model’s inheritance_column value for STI. (default: versioned_type)

  • version_column - name of the column in the model that keeps the version number (default: version)

  • sequence_name - name of the custom sequence to be used by the versioned model.

  • limit - number of revisions to keep, defaults to unlimited

  • if - symbol of method to check before saving a new version. If this method returns false, a new version is not saved. For finer control, pass either a Proc or modify Model#version_condition_met?

    acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
    

    or…

    class Auction
      def version_condition_met? # totally bypasses the <tt>:if</tt> option
        !expired?
      end
    end
    
  • if_changed - Simple way of specifying attributes that are required to be changed before saving a model. This takes either a symbol or array of symbols.

  • extend - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block to create an anonymous mixin:

    class Auction
      acts_as_versioned do
        def started?
          !started_at.nil?
        end
      end
    end
    

    or…

    module AuctionExtension
      def started?
        !started_at.nil?
      end
    end
    class Auction
      acts_as_versioned :extend => AuctionExtension
    end
    
Example code:

  @auction = Auction.find(1)
  @auction.started?
  @auction.versions.first.started?

Database Schema

The model that you’re versioning needs to have a ‘version’ attribute. The model is versioned into a table called #model_versions where the model name is singlular. The _versions table should contain all the fields you want versioned, the same version column, and a #model_id foreign key field.

A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance, then that field is reflected in the versioned model as ‘versioned_type’ by default.

Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table method, perfect for a migration. It will also create the version column if the main model does not already have it.

class AddVersions < ActiveRecord::Migration
  def self.up
    # create_versioned_table takes the same options hash
    # that create_table does
    Post.create_versioned_table
  end

  def self.down
    Post.drop_versioned_table
  end
end

Changing What Fields Are Versioned

By default, acts_as_versioned will version all but these fields:

[self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]

You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.

class Post < ActiveRecord::Base
  acts_as_versioned
  self.non_versioned_columns << 'comments_count'
end


163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/acts_as_versioned.rb', line 163

def acts_as_versioned(options = {}, &extension)
  # don't allow multiple calls
  return if self.included_modules.include?(ActiveRecord::Acts::Versioned::Behaviors)

  cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
                 :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
                 :version_association_options, :version_if_changed, :version_except_columns

  self.versioned_class_name         = options[:class_name] || "Version"
  self.versioned_foreign_key        = options[:foreign_key] || self.to_s.foreign_key
  self.versioned_table_name         = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
  self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
  self.version_column               = options[:version_column] || 'version'
  self.version_sequence_name        = options[:sequence_name]
  self.max_version_limit            = options[:limit].to_i
  self.version_condition            = options[:if] || true
  self.version_except_columns       = [options[:except]].flatten.map(&:to_s)  #these columns are kept in _versioned, but changing them does not excplitly cause a version change
  self.non_versioned_columns        = [self.primary_key, inheritance_column, self.version_column, 'lock_version', versioned_inheritance_column] #these columns are excluded from _versions, and changing them does not cause a version change
  self.version_association_options  = {
                                              :class_name  => "#{self.to_s}::#{versioned_class_name}",
                                              :foreign_key => versioned_foreign_key,
                                              :dependent   => :delete_all
  }.merge(options[:association_options] || {})

  if block_given?
    extension_module_name = "#{versioned_class_name}Extension"
    silence_warnings do
      self.const_set(extension_module_name, Module.new(&extension))
    end

    options[:extend] = self.const_get(extension_module_name)
  end

  unless options[:if_changed].nil?
    self.track_altered_attributes = true
    options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
    self.version_if_changed = options[:if_changed].map(&:to_s)
  end

  include options[:extend] if options[:extend].is_a?(Module)

  include ActiveRecord::Acts::Versioned::Behaviors

  #
  # Create the dynamic versioned model
  #
  const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
    def self.reloadable?;
      false;
    end

    # find first version before the given version
    def self.before(version)
      where(["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]).
              order('version DESC').
              first
    end

    # find first version after the given version.
    def self.after(version)
      where(["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]).
              order('version ASC').
              first
    end

    # finds earliest version of this record
    def self.earliest
      order("#{original_class.version_column}").first
    end

    # find latest version of this record
    def self.latest
      order("#{original_class.version_column} desc").first
    end

    def previous
      self.class.before(self)
    end

    def next
      self.class.after(self)
    end

    def versions_count
      page.version
    end
  end

  versioned_class.cattr_accessor :original_class
  versioned_class.original_class = self
  versioned_class.table_name = versioned_table_name
  versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
                             :class_name  => "::#{self.to_s}",
                             :foreign_key => versioned_foreign_key
  versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
  versioned_class.set_sequence_name version_sequence_name if version_sequence_name
end