Manipulating Record Versions with acts_as_versioned

Problem

You want to let users view or revert versioned changes made to the rows in your database.

Solution

Use the acts_as_versioned plug-in to track changes made to rows in a table and to set up a view that allows access to the a revision history.

Start by installing the plug-in within your application:

$ ./script/plugin install acts_as_versioned

Set up a database to store statements and to track changes made each statement. For versioning to work, the table being tracked needs to have a version column of type :int.

db/migrate/001_create_statements.rb:

class CreateStatements < ActiveRecord::Migration
 def self.up
 create_table 'statements' do |t|
 t.column 'title', :string
 t.column 'body', :text
 t.column 'version', :int
 end
 end
 def self.down
 drop_table 'statements'
 end end

Now, create a second table named statement_versions. The name of this table is based on the singular form of the name of the table being versioned, followed by the string _versions. This table accumulates all versions of the columns you want to track. Specify those columns by adding columns to the statement_versions table, each having the same name and datatype as the columns in the table you're tracking. The statement_versions table needs to have a version column of type :int as well. Next, add a column referencing the versioned table's id field, e.g., statement_id.

db/migrate/002_add_versions.rb:

class AddVersions < ActiveRecord::Migration
 def self.up
 create_table 'statement_versions' do |t|
 t.column 'statement_id', :int
 t.column 'title', :string
 t.column 'body', :text
 t.column 'version', :int
 end
 end
 def self.down
 drop_table 'statement_versions'
 end end

Finally, set up the Statement model to be versioned by calling acts_as_versioned in its class definition:

app/models/statement.rb:

class Statement < ActiveRecord::Base
 acts_as_versioned end

Now, changes made to Statement objects automatically update the object's version number and save current and previous versions in the statement_versions table. Being versioned, Statement objects gain a number of methods that allow for inspection and manipulation of versions. To allow users to revert versions, you can modify your Statements controller, adding a revert_version action:

def revert_version
 @statement = Statement.find(params[:id])
 @statement.revert_to!(params[:version]) 
 redirect_to :action => 'edit', :id => @statement end

Modify the Statement edit view, adding linked version numbers that revert changes by calling the revert_version action.

app/views/edit.rhtml:

<h1>Editing statement</h1>
<% form_tag :action => 'update', :id => @statement do %>
 <%= render :partial => 'form' %>
 <p><label for="statement_version">Version</label>: 
 <% if @statement.version > 0 %>
 <% (1..@statement.versions.length).each do |v| %>
 <% if @statement.version == v %> 
 <%= v %>
 <% else %>
 <%= link_to v, :action => 'revert_version', :id => @statement, \
 :version => v %>
 <% end %>
 <% end %>
 <% end %>
 </p>
 <%= submit_tag 'Edit' %>
<% end %>
<%= link_to 'Show', :action => 'show', :id => @statement %> |
<%= link_to 'Back', :action => 'list' %>

Discussion

You can use the Rails console to test a basic update and reversion session on a Statement object:

>> statement = Statement.create(:title => 'Invasion', :body => 'because of WMDs')
=> #<Statement:0x22f0c94 @attributes={"body"=>"because of WMDs", 
"title"=>"Invasion", "id"=>6, "version"=>1}, @new_record=false, 
@changed_attributes=[], @new_record_before_save=true, 
@errors=#<ActiveRecord::Errors:0x22ef1b4 @base=#<Statement:0x22f0c94 ...>, 
@errors={}>>
>> statement.version
=> 1
>> statement.body = 'opp! no WMDs'
=> "opp! no WMDs"
>> statement.save
=> true
>> statement.version
=> 2
>> statement.revert_to!(statement.version-1)
=> true
>> statement.body
=> "because of WMDs"
>> statement.version
=> 1

shows the statement edit page. It includes links to all previous versions that call the revert action. Submitting the form using the edit button will add a new version number.

Figure 14-1. An edit form that displays links to previous versions

You can alter the default behavior by passing an option hash to the acts_as_versioned method. For example, :class_name and :table_name can be set if the default naming convention isn't suitable for your project. Another useful option is :limit, which specifies a fixed number of revisions to keep available.

See Also