homeASCIIcasts

177: Model Versioning 

(view original Railscast)

Other translations: Cn

In this episode we’re building a wiki-style application. The application has a page model and below is the show action for the home page.

Our wiki's home page.

Like most wikis, anyone can edit one of the pages and change the content, but one significant feature that’s missing from ours is the ability to go back and see previous versions of the pages. What we need is a versioning solution for our models.

Vestal Versions

Episode 3 of the Ruby5 podcast featured a plugin that can help with this problem. Vestal Versions stores a history of the changes that are made to ActiveRecord models, which is exactly what we’re looking for for our wiki.

Vestal Versions is available as a gem, so to use it in our application it we just have to add

config.gem 'laserlemon-vestal_versions', :lib => 'vestal_versions', :source => 'http://gems.github.com'

into the config block in /config/environment.rb then run

sudo rake gems:install

to make sure that the gem is installed.

As Vestal Versions needs to make changes to our app’s database the next step is to run its migration generator.

script/generate vestal_versions_migration

This generates the migration for the versions table that Vestal Versions uses so next we’ll have to migrate the database.

rake db:migrate

Now we can add versioning to our Page model. To do this all we have to do is call versioned from within our model.

class Page < ActiveRecord::Base
  versioned
end

That’s it. Any changes made to a Page will now be tracked within the versions table.

Adding Versioning To Our Wiki Pages

The first change we’ll make to our wiki application is to display the version number on the show action page. To do this we just need to show the page’s version.

<% title @page.name %>
<p><%= simple_format wiki_link(h(@page.content))%></p>
<p>
  <%= link_to "Edit", edit_page_path(@page) %>
  | Version <%= @page.version %>
</p>

When we reload the page, however, the version number isn’t shown.

The version number isn’t displayed.

This has happened because the page existed before we added Vestal Versions to the application and therefore hadn’t had a version applied to it. We can fix this by updating the existing models which will cause Vestal Versions to apply versioning to them.

We could do this in the console if we just want to change the development version of our app but we want to ensure that the versioning is set everywhere that this code is deployed. To make sure of this we’ll generate a migration instead.

script/generate migration version_existing_pages

In the migration we want to update any existing Page records. As our application is running on Rails 2.3.3 we can make use of the new touch functionality to simply update each one.

class VersionExistingPages < ActiveRecord::Migration
  def self.up
    say_with_time "Setting initial version for pages" do
      Page.find_each(&:touch)
    end
  end

  def self.down
  end
end

Calling touch on a page will update its timestamp and will therefore trigger the versioning. Note that we’re using say_with_time to output a message from the migration to the console.

We can now run

rake db:migrate

again and each page will have a version applied to it.

If we go back to our application now and reload the page we’ll see that the version number is now displayed.

The version number is now showing.

And when we edit the page, the version number should change to 2.

When we update the page the version number increases.

It does, and we’re now displaying version 2 of the home page.

As our pages are now versioned it would be good if we could add a link on the page to display the previous version next to the current version number. In our show view we can add the link after the code that displays the version number.

  <% if @page.version > 1 %>
  | <%= link_to "Previous version", :version => @page.version-1 %>
  <% end %>

The if condition ensures that the link is only shown if the page has a previous version to show. As for the link itself, we want to display the same page, but with a version parameter in the URL that has the value of the previous version of the page. Then. in the PagesController’s show action we can check for the version parameter and, if we find it, go to the appropriate version of that page.

def show
  @page = Page.find(params[:id])
  @page.revert_to(params[:version].to_i) if params[:version]
end

In the show action we use Vestal Versions’ revert_to method to get the correct version of the page if a version parameter has been passed. One thing to note is that revert_to expects an integer value and the parameter will be a string so we need to cast it first.

When we reload the page again we’ll now see the “Previous Version” link.

Adding a “previous version” link.

And when we click it we’ll get the first version of the page.

Looking at a previous version of the page.

When looking at an older version of a page it would be handy to have a link back to the current version. We can do that by adding this code to our view.

<% if params[:version] %>
  | <%= link_to "Latest version", :version => nil %>
<% end %>

Now, when we’re viewing a previous version of the page (i.e. when a version parameter exists in the querystring) we’ll be shown a link back to the current version of the page.

Older versions now have a link to the current version.

Some Other Tricks

There are a number of other things that Vestal Versions can do, which we’ll demonstrate in the console. (Note that we’re using hirb here to prettify the output.) If we get the first page

>> p = Page.first
+----+----------+--------------------+--------------------+--------------------+
| id | name     | content            | created_at         | updated_at         |
+----+----------+--------------------+--------------------+--------------------+
| 1  | HomePage | Welcome to out ... | 2009-08-31 08:4... | 2009-08-31 10:1... |
+----+----------+--------------------+--------------------+--------------------+
1 row in set

we then can call a versions method on that page to get all of its versions.

>> p.versions
+----+--------------+---------------+---------------+--------+---------------+
| id | versioned_id | versioned_... | changes       | number | created_at    |
+----+--------------+---------------+---------------+--------+---------------+
| 1  | 1            | Page          | nameHomePa... | 1      | 2009-08-31... |
| 2  | 1            | Page          | updated_at... | 2      | 2009-08-31... |
+----+--------------+---------------+---------------+--------+---------------+
2 rows in set

versions is set up as an ActiveRecord association (and is itself its own model) which maps to the versions table we created earlier with the migration. We could, for example, use this to have a history page that showed each version of a Page and link to each one.

Earlier we used revert_to to show an earlier version of a page, passing it the id of the version we wanted. We can pass revert_to different parameters, though, including a datetime value. To get the version we had half an hour ago we can pass 30.minutes.ago and we should see the first version of the page we created.

>> p.revert_to(60.minutes.ago)
=> 1
>> p.content
=> "Welcome to out humble wiki where you can learn how to PurchasePianos,
WriteMusic and PlayPiano. If you're just getting started, check out the
BeginnerPiano page. Enjoy!"

We can also pass the symbols :first or :last to get the oldest or newest versions of a model.

>> p.revert_to(:first)
=> 1
>> p.revert_to(:last)
=> 2

That’s all for this episode. Hopefully we’ve shown you that Vestal Versions is a great solution for any situation in which you need to keep a history of changes to your ActiveRecord models in your Rails applications.