Acts_as_versioned tutorial
June 26, 2009
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
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:
.tables versions
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:
select * from artist_versions;
5 – Now add version functionality to your controller / view:
To show the list of versions:
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. .
3 Comments Add your own
Leave a Comment
Some HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
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#updateundefined 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.