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 [/sourcecode] 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: [sourcecode language='sql'] .tables versions [/sourcecode] 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: [sourcecode language='sql'] select * from artist_versions; [/sourcecode] 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.
6 Comments Add your own
Leave a comment
Trackback this post | Subscribe to the comments via RSS Feed
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?
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.
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.
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
5. Satts | April 29, 2010 at 12:49 pm
Though there was confustion with the routes.
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!