#177 Model Versioning
- Download:
- source codeProject Files in Zip (87.1 KB)
- mp4Full Size H.264 Video (12.4 MB)
- m4vSmaller H.264 Video (9.04 MB)
- webmFull Size VP8 Video (25.1 MB)
- ogvFull Size Theora Video (18.7 MB)
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.
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.
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.
And when we edit the page, the version number should change to 2.
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.
And when we click it we’ll get the first 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.
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.