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

Defined in:
lib/junebug/ext/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


155
156
157
158
159
160
161
162
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
# File 'lib/junebug/ext/acts_as_versioned.rb', line 155

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_changed_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, :changed_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}",
                                        :order       => 'version',
                                        :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
    before_save  :set_new_version
    after_create :save_version_on_create
    after_update :save_version
    after_save   :clear_old_versions
    after_save   :clear_changed_attributes
    
    unless options[:if_changed].nil?
      self.track_changed_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
  end
  
  versioned_class.set_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