Module: ActiveRecord::Acts::Versioned::ClassMethods

Defined in:
lib/plugins/acts_as_versioned/lib/acts_as_versioned.rb

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. WARNING - This will attempt to overwrite any attribute setters you may have. Use this instead if you want to write your own attribute setters (and ignore if_changed):

    def name=(new_name)
      write_changed_attribute :name, new_name
    end
    
  • 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

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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/plugins/acts_as_versioned/lib/acts_as_versioned.rb', line 170

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

  send :include, ActiveRecord::Acts::Versioned::ActMethods

  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

  # legacy
  alias_method :non_versioned_fields,  :non_versioned_columns
  alias_method :non_versioned_fields=, :non_versioned_columns=

  class << self
    alias_method :non_versioned_fields,  :non_versioned_columns
    alias_method :non_versioned_fields=, :non_versioned_columns=
  end

  send :attr_accessor, :altered_attributes

  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.non_versioned_columns        = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
  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

  class_eval do
    has_many :versions, version_association_options do
      # finds earliest version of this record
      def earliest
        @earliest ||= order('version').first
      end

      # find latest version of this record
      def latest
        @latest ||= order('version desc').first
      end
    end
    before_save  :set_new_version
    after_create :save_version_on_create
    after_update :save_version
    after_save   :clear_old_versions
    after_save   :clear_altered_attributes

    unless options[:if_changed].nil?
      self.track_altered_attributes = true
      options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
      options[:if_changed].each do |attr_name|
        define_method("#{attr_name}=") do |value|
          write_changed_attribute attr_name, value
        end
      end
    end

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

  # 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)
      order('version desc').
        where("#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version).
        first
    end

    # find first version after the given version.
    def self.after(version)
      order('version').
        where("#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version).
        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