Acts_as_versioned tutorial

June 26, 2009 at 5:55 pm 6 comments

Environment: Rails 2.3.2, Windows Vista, SQLite3 development on a local box

You might want to create a wiki-like functionality on your web site that is able to save all changes to your objects and adds an ability to retrieve them. So, for example you have pages for Artists that anybody can change in wiki-fashion but you also want to save the history of such changes and revert to a specific version as needed.

Good thing is that there is a very neat plugin called “acts_as_versioned” that you can install and gain access to versions of your objects straight away. Bad thing is that most of the tutorials and documentation is outdated (the official RDoc was last updated in 2005).

So, if you are trying to follow some tutorials or recipes you might get an error:

NoMethodError: undefined method `find_version'
or 
NoMethodError: undefined method `find_versions'

So use this tutorial to get through this.

1 – First, download the latest version of the plugin acts_as_versioned from here. Do not use the script/install command, as it downloads the old version that does not work any more with Rails 2.2 or 2.3. You can manually install the plugins by copying the files in the directory to a subdirectory “acts_as_versioned” in the path vendor/plugins. Don’t forget to restart the script/server, otherwise RoR does not see your new plugin.

While there, you can also generate the up to date documentation by running:

rake doc:plugins:acts_as_versioned

This will place the documentation for the plugin in the doc/plugin subdirectory of your project.

2 – In the model for which you want version support add the following line somewhere closer to the top:

acts_as_versioned

If you want to add it for a model “Artists”, then you should edit app/models/artist.rb

3 – Now you need to create a table which will store the older versions of your objects. This is done by running the following command:

Artist.create_versioned_table

You can put it in a db migration task or you can run it in a script/console. The best way is to add it to migrations and you can use the following migration (by running rake db:migrate)

  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
&#91;/sourcecode&#93;

4 - Now try out how it works - the objects should be saved and shown just as before, but at the same time a copy of your object should be saved into artist_versions table. To check it, you can use sqlite3 console by either running script/dbconsole or if it gives you an error (as it does for me running on vista), then you can run sqlite3 development.sqlite3 in the db subdirectory of your project

In sqlite3 check the following:
&#91;sourcecode language='sql'&#93;
.tables versions
&#91;/sourcecode&#93;
This will show you all the tables with "versions" in the name. There should be your "artist_versions" table. Check that the object is saved correctly there by running:
&#91;sourcecode language='sql'&#93;
select * from artist_versions;
&#91;/sourcecode&#93;

5 - Now add version functionality to your controller / view:

<strong>To show the list of versions:</strong>

In artist_controller.rb show method you can add the following:


@artistversion = @artist.versions.find(:all)

This will populate @artistversion with the array of versions of the current object @artist. And that’s the new syntax that replaces the old find_versions / find_version that might give you trouble.

And in the view you should add something like that:

	<b>Edits history:</b><br/>
	<% @artistversion.reverse.each do |version| %>
  Version <%= version.version %> Updated: <%= version.updated_at %>
  <%= link_to '(restore)', :action => 'restore', 
                   :version_id => version.version, 
                   :artist_id => @artist.id %><br/>
<% end %>

Version variable has access to all the attributes of the object and also to the attribute “version” that you can use. You can just as easily show who was the editor of the version if you store that information by using version.user_id, etc. Also, as artistversion is just an array, it is nice if you reverse the order, so that the latest version is shown first.

Finally, add the method restore to your controller:

def restore
   @artist = Artist.find(params[:artist_id])
   @artist.revert_to! params[:version_id]
   redirect_to :action => 'show', :id => @artist
end  

revert_to! method restores the object to a given version and saves it.

You might need to also update the routes.rb if you are using default RESTful map if you get a “unknown method” error:

Your route might look something like this:

map.resources :artists, :collection => {:restore => :get}

Additional settings

Another cool feature is to trigger the saving of a version only if particular fields are changed. It’s easy to set up:

acts_as_versioned :if_changed => [:name, :description, :user_id]

You can also use if option, that allows you to save a version only if method called returns true. Here is the extract from documentation:

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? }

Limitations

So, acts_as_versioned lets you create a history of changes to your model. What it does not do is save any related changes that are not exactly a part of the main class table, for example if you have tags or pictures stored in the separate classes/tables.

Also, if you delete the main object, all the object versions are automatically deleted. This means that you would lose the whole object and all of it’s versions – very open to abuse, etc. In order to circumvent this behavior you can use another plugin – acts_as_paranoid, instructions are provided in this blog.

Finally, if you decide to change the main object (add table columns), the versions table is not updated automatically, you will need to add new columns to it yourself.

Entry filed under: Uncategorized.

Transactions Post ruby or html, css, sql sourcecode in your wordpress.com blog

6 Comments Add your own

  • 1. gemp  |  July 14, 2009 at 7:27 pm

    Nice tutorial, as well as your other posts on the subject, thanks.

    But I can’t seem to make it work… I was in Rails 2.0.2, I upgraded everything in case Rails 2.3.2 now, even created a new project from scratch, also checked with MySQL, to no avail.

    When I migrate, I get the error:
    undefined method `table_exists?' for #

    I tried to create the table and columns manually, I get a similar error:
    NoMethodError in ChaptersController#update
    undefined method `changed?'

    Couldn’t find any information anywhere about that, it’s unnerving.

    Any idea why?

    Reply
  • 2. gemp  |  July 14, 2009 at 8:54 pm

    Damned, I was still in 2.0.2 apparently.

    Works in 2.3.2. You could delete those two comments — or not, it might be helpful for others…

    That was really a pain.

    Reply
    • 3. allaboutruby  |  July 19, 2009 at 10:51 am

      Good to see you sorted that out. That’s usually the type of errors that give the most headache.

      Reply
  • 4. Satts  |  April 29, 2010 at 11:52 am

    @allaboutruby

    you have been a saviour. This post was really really helpful and the process worked without error

    Reply
  • 5. Satts  |  April 29, 2010 at 12:49 pm

    Though there was confustion with the routes.

    Reply
  • 6. Hiro Goto  |  June 11, 2011 at 12:19 am

    This work well at the simple table. But I want to work this at related two table. I saw a hint at
    http://old.nabble.com/acts_as_versioned-and-getting-authors-td2922938.html
    It say that
    >> This will work for me. My natural followup questions to this is
    >> how can two versioned classes be linked together? Assuming Parent
    >> has_many Children and a Child belongs_to Parent, I would want
    >> Parent_version to have_many Children_Version and a Child_Version
    >> to belong_to Parent_version. Is this easily doable ?

    > Sure, just reopen the class at the bottom of the file.

    > class Foo acts_as_versioned
    > end

    >Foo.versioned_class.class_eval do
    > # extra stuff
    >end

    My env.
    rails 3.0.7
    active Record 3.0.7
    acts as vesioned 0.6.0

    Please teach me a idea, what I do at “#extra stuff”.
    Thanks!

    Reply

Leave a comment

Trackback this post  |  Subscribe to the comments via RSS Feed


Starting to learn Rails?

Kindle

Get Kindle - the best e-book reader, that I personally use, and the only one that you can read on the beach - very useful: Kindle Wireless Reading Device (6" Display, Global Wireless, Latest Generation)