Lately, I've been learning Ruby on Rails. In layman's terms, Ruby on Rails is a system for making dynamic websites like Flickr, Friendster, and yes, Music For Dozens. To drastically oversimplify, some of the smart people have gone ahead and handled all of the common tasks like logging on, handling user input from forms, remembering data and displaying it when appropriate, etc. The idea is to make it easier to create a dynamic website by letting you focus just on the unique thing that you want your site to do and not all the boring and difficult infrastructure all such sites need.
Chris and I are writing a super-secret exciting new MFDZ project in Rails and, eventually, we'll be rewriting MFDZ itself in it. In the process, I've come across some opportunities to actually get paid for working on other people's Rails projects, so I've been devoting some semi-serious time to improving my skills. Predictably, I've encountered a few beginner-type problems to which I couldn't find great solutions online. Some of these I've been able to work through with the online help of some of the many strong Rails programmers in Portland and a few others I managed to solve with just my own wits. I figured it would be the neighborly thing to do to write down some of what I've learned here for the sake of the next poor soul who would otherwise Google-up empty on the same problem.
Thus, I introduce learns_to, a new category of post here at IDFDZ where I'll try to document a little bit of what I've learned about Rails as clearly and concretely as I can. If you're one of the people who emails me about my blog becoming too boring and "technical" or, more specifically, you're just not interested in Rails, I urge you to move on now. But, if you're me from earlier this week -- wondering how you'll ever get acts_as_versioned to work with your project when you barely know the difference between a plugin and a gem -- then hopefully you've come to just the right place.
What it is
Acts_as_versioned is a chunk of Rails code meant to help you keep track of, well, progressive versions of whatever it is your application keeps track of. The quintessential example is the way a wiki keeps track of edits made to a page's information. In fact, acts_as_versioned is great for giving wiki-like revisible user-edited content to any part of your Rails app.
Where to get it</h4
As I alluded to in the introduction, there are two different ways to get your hands on acts_as_versioned: as a plugin or as a gem. RubyGems is Ruby's built-in packaging system. It has a number of conveniences including easy command line installation and updating. These characteristics make it definitely the best way to get Rails itself and to keep your local copy fresh. However, in the context of an add-on like acts_as_versioned, I would recommend using plugins whenever you have the opportunity. Plugins have the great advantage that they are automatically included by Rails itself when you start the server. You don't have to write any fancy setup code. That means not having to venture into environment.rb, or any other exotic places that can be scary for Rails newbies like me.
So, go ahead and download acts_as_versioned now (that link is to a .tgz, just what you'll need on the mac). Unzip it and move it into the /vendor/plugins folder in your Rails app and we'll get started using it.
Database setup
Before we get too far into the workings here, I should point out the documentation. It is not especially human readable, but it is a definitive reference for this stuff (most of what I'm about to say, I figured out by staring at the documentation for the Class Methods for a long time).
Anyway, the first thing we've got to do is get our system setup to use acts_as_versioned. This means database setup and some small changes to our models. Let's say we're doing a music app. We've got artists and we're representing them in an 'artists' table and a corresponding Artist class. Our artists have names and bios and, of course, ids. That means that our migration for creating our table looks like this (pre-versioning):
create_table :artists do |t|
t.column :name, :string
t.column :bio, :text
end
(If you're not familiar with migrations, they're the system Rails provides for representing your database structure and, especially, changes you make to it in code. They are super convenient. The Understanding Migrations tutorial on the Rails wiki is a great place to get started with them and, if you have any further questions, the Rails migration documentation is comprehensive. The sooner you start using migrations, the sooner you'll fall in love with Rails. Note: When you use migrations to set up your database, Rails adds ids automatically wherever they belong, which is why I didn't specify one here.)
Now, acts_as_versioned is going to mirror our artist data into a parallel table called 'artist_versions'. Each row of that table will represent a subsequent state, or version, of each of our artists. So we need to create the artist_versions table with a version column, a foreign key to tell it which artist its keeping track of (in this case we'll call that one 'artist_id'), and a column for each bit of data we want to version from our original model; for now, let's just do bio. All this adds up to a migration that looks like this:
Artist.create_versioned_table do |t|
t.column :version, :integer
t.column :artist_id, :integer
t.column :bio, :text
end
So the obvious thing to point out here is that we're using a new method "create_versioned_table" and that it's a method on the Artist model itself. In order for this to work, then, we're going to have to tell our Artist model that something's going on with versions. It's super easy; just one line: 'acts_as_versioned' within the Artist class. I like to keep it up near the relation declarations at the top so I don't lose track of it. Once we add that, the plugin does the rest of the work, adding a whole boatload of methods to our model including this migration method, create_versioned_table (and the converse we'll use in the down part of our migration: "Artist.drop_versioned_table"). Again, if you're doing this by hand, remember that our migration automatically adds an id to our table.
Assuming we've combined these two bits together into a migration and run it, we'll now have our database properly in place and we can start using all the methods the acts_as_versioned plugin has added to our Artist model.
Using the methods provided by acts_as_versioned
As you'll soon learn if you spend some time with the documentation, acts_as_versioned provides methods to do most things you can think of with it. I'm only going to go into detail here on the two that I think are most useful: revert_to, for rolling back to previous versions of your model and find_versions which is great for displaying old states of your data.
Let's start with find_versions. If @artist is an instance variable containing a particular artist then in our view we can do something like:
<% for version in @artist.find_versions.reverse %>
Version <%= version.version %> <br />
<%= link_to '(revert to this version)',
:action => 'revert_to_version',
:version_id => version.id,
:artist_id => @artist %>
<% end %>
This view code iterates over all the saved versions of our artist (starting with the most recent and heading backwards) displaying the version number and then providing a link to revert to that version. Other similar methods will let you find specific versions that meet given criteria or to get at just the particular attributes that you're keeping versioned.
Now, let's look closer at that link_to call. It's calling a custom controller action called 'revert_to_version', passing in the id of the version we want to revert to and the id of our artist. We want this link to revert the artist we've got stored in @artist to the version whose id we're passing in. The controller code necessary to do this will use the revert_to method provided by acts_as_versioned, like so:
def revert_to_version
@artist = Artist.find( params[:artist_id] )
@artist.revert_to! params[:version_id]
redirect_to :action => 'show', :id => @artist
end
All we're doing here grabbing ahold of our artist instance using the normal class 'find' method. Then we just call revert_to! (we use the conventional exclamation mark syntax to save as well as reverting) with the version_id as an argument and the old version of the artist's bio will now be saved in the right place in the artists table. One nice thing about using acts_as_versioned in this way is that it is non-destructive; all the more recent versions since the one to which we just reverted are still saved in our artist_versions table and we can always un-revert to them (if that's not too confusing).
And that is pretty much an introduction to acts_as_versioned. I've just scratched the surface of the subtle things you can do with it, especially when it comes on setting conditions for saving new versions. I've said it twice before, but a third time couldn't hurt: read the documentation. There's lots to learn.
Many thanks to Rick Olson, the author of acts_as_versioned, both for his great plugin and his rapid, clear, and helpful support when I was first trying to use it myself.
Tagged: rails, ruby, acts_as_versioned, wiki, plugin, gem, tutorial